The simpliest way to subscribe for an observableArray in knockoutjs - google-maps

I have an observableArray with some markers, for example:
var markers = ko.observableArray([
{
id: 0,
title: ko.observable("Marker 0"),
lat: ko.observable(55.31),
lng: ko.observable(11)
},
{
id: 1,
title: ko.observable("Marker 1"),
lat: ko.observable(57.20),
lng: ko.observable(15.5)
}
]);
This array is sent to some MapWidget object, that has to create google map markers for each element. It has to move markers in case lat,lng observables change, change marker's title in case title observable changes and so on.
That means in MapWidget there's some array of googlemap markers, and it should be connected with the given observableArray. What is the best and the simpliest way to connect them?
Upd. More details about MapWidget.
MapWidget is some object that has an access to some google maps map object, and it receives as an argument an observableArray with markers like that one above.
var MapWidget = function(markers) {
var div = $("#map").get(0);
this.map = new gmaps.Map(div);
/*
The magic goes here:
markers is an observableArray, we subscribe for it's changes,
create gmaps.marker for each new element,
destroy in case of destroying them from array,
move and rename each marker in case of corresponding changes
*/
}

You could subscribe to your array like this:
ar.subscribe(function() {
// clean and redraw all markers
});
If you do that, you will receive a notification when an item is added/removed to/from the array. BUT not when an property of an item is modified.
If you want to update your google maps markers based on individual property changes in items, you could implement a simple dirty flag mechanism and then subscribe individually to each item. Since each item has an id, which I presume is unique, you could create a map with key/value pairs being the id and the map widget.
So, given each individual item:
var item = function(data) {
var self = this;
self.isChanged = ko.observable(self);
self.id = data.id;
self.title = ko.observable(data.title);
self.title.subscribe(function() { self.isChanged(self); self.isChanged.valueHasMutated(); });
self.lat = ko.observable(data.lat);
self.lat.subscribe(function() { self.isChanged(self); self.isChanged.valueHasMutated(); });
self.lng = ko.observable(data.lng);
self.lng.subscribe(function() { self.isChanged(self); self.isChanged.valueHasMutated(); });
}
And given a hypothetic map where you keep a link between your markers and the items:
var markersMap = [];
markersMap[0] = yourGoogleMapWidget;
You can then subscribe to track changes on items like this:
ar[0].isChanged.subscribe(function(item) {
var myGMapMarker = markersMap[item.id()];
// update your marker, or destroy and recreate it...
});

Related

Flutter update element create by ui.platformViewRegistry.registerViewFactory class

I am working with Google Map for flutter Website. As most tutorial said that, the map is displayed through ui.platformViewRegistry.registerViewFactory
below is the code example. As you see, i create the widget with the variable and tried to give the _lat and _lng from outside. But after i tried to give the value, the ui.platformViewRegistry.registerViewFactory is not triggered.
I just realize that the ui.platformViewRegistry.registerViewFactory objected to create the element so when the element was created, it will not executed again, but I cannot access the element via document.getElementById('map-canvas') either.
Anyone have idea about this?
Widget getMap(double _lat, double _lng) {
String htmlId = "map-canvas";
ui.platformViewRegistry.registerViewFactory(htmlId, (int viewId) {
// final myLatLng = LatLng(-25.363882, 131.044922);
final mapOptions = new MapOptions()
..zoom = 8
..center = new LatLng(_lat, _lng)
..mapTypeControl = false;
final elem = DivElement()
..id = htmlId
..style.width = "100%"
..style.height = "100%"
..style.border = "none";
final map = new GMap(elem, mapOptions);
Marker(MarkerOptions()
..position = LatLng(_lat, _lng)
..map = map
..title = 'Green Energy');
return elem;
});
return HtmlElementView(
viewType: htmlId,
);
}
I found two ways to solve this problem.
It will update the whole view when you change the htmlId and reload.
Either determine how the htmlId should change or give it a new random number with every reload. String htmlId = Random().nextInt(1000).toString();
The disadvantage here is that you always reload the whole map.
This is the better solution as you dont have to reload the complete map. I also had the problem that i could change values inside the ui.platformViewRegistry.registerViewFactory function with data i give to the getMap widget.
So the first part of this solution is to create a global variable that you use inside the ui.platformViewRegistry.registerViewFactory function but update outside.
The second part is to use listener functions inside the ui.platformViewRegistry.registerViewFactory. For example map.onClick.listen((mapsMouseEvent) {}); or map.onZoomChanged.listen((_) {}); or a Stream ANY_STREAM_CONTROLLER.stream.listen((data) {}).
If you would like to add new markers when you zoom out you could do it something like this
ui.platformViewRegistry.registerViewFactory(htmlId, (int viewId) {
map.onZoomChanged.listen((_) {
List<LatLng> marker_list = list_of_markers(map.zoom, map.center);
//Function that gives you a list of markers depending on your zoom level and center
marker_list.forEach((latlng){
Marker marker = Marker(MarkerOptions()
..position = latlng
..map = map
..title = myLatlng.toString()
..label = myLatlng.toString());
});
});
}
I hope the solution helps you

How to reset coordinates into an Ajax call after having initialized a map?

I inserted a map on my webpage by using the Leaflet library. What I want to do is to show a map zoomed on a specific region according to which city the user types into a text field.
I firstly initialized my map on my JS file:
function initMaps(){
map = L.map('leaflet').setView([0, 0], 13);
L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
'attribution': 'Map data © OpenStreetMap contributors'
}).addTo(map);
}
My javascript code also has an Ajax call.
What I want to do now is to reset the coordinates on the Ajax call.
I wrote the following:
var readCoordinates = function(){
$.ajax({
url: "https://nominatim.openstreetmap.org/search?q=" + encodeURIComponent($("#inlineFormInputCitta").val()) + "+Italy&format=geocodejson",
dataType: "json",
success: function (data) {
setTimeout(function () {
for (let i = 0; i < data.features.length; i++) {
let coordinate = data.features[i].geometry.coordinates;
console.log(coordinate);
map.setView(coordinate, 13);
console.log("ajax and for loop have been activated");
console.log(coordinate.geometry.coordinates);
};
$("#ristoranti").prop("disabled", false);
}, 1000);
}
});
};
The API I'm referring to in the URL is the following: https://nominatim.openstreetmap.org/search?q=Roma%20Italy&format=geocodejson
What I did is trying to reset the coordinates here: map.setView(coordinate, 13);
after having cycled the elements in the JSON object, see the following:
for (let i = 0; i < data.features.length; i++) {
let coordinate = data.features[i].geometry.coordinates;
I may display several coordinates in the console, see the following:
That's because in the JSON file I get through the API request there are several:
The result of this is the following map, which isn't zoomed anywhere:
Which coordinates should I take in order to display that specific region?
EDIT - - -
I changed the code because I'm trying to get a specific subobject, i.e. the one in the screen below (which has "type" = "city"):
The new snippet is the one below, where I add an if statement:
var readCoordinates = function(){
$.ajax({
url: "https://nominatim.openstreetmap.org/search?q=" + encodeURIComponent($("#inlineFormInputCitta").val()) + "+Italy&format=geocodejson",
dataType: "json",
success: function (data) {
setTimeout(function() {
for (let i = 0; i < data.features.length; i++) {
debugger;
let type = data.features[i].properties.geocoding.type;
if( $(type).val() === "city") {
let coordinate = data.features[i].geometry.coordinates;
let lng = coordinate[0];
let lat = coordinate[1];
map.setView([lng, lat], 13);
console.log("ajax and for loop have been activated");
console.log(coordinate);}
};
$("#ristoranti").prop("disabled", false);
}, 1000);
}
});
};
I'm doing the debugger and get many undefined values:
I would do something like that:
if (typeof data.features[0] !== 'undefined') {
let coordinate = data.features[0].geometry.coordinates;
var latlng = L.latLng(coordinate.reverse());
map.flyTo(latlng, 12)
}
Be sure to have something in your array
Get data from the first item since it should be the correct one in most case
Create a latlng with those coordinates. Be careful, sometime you need to reverse the array to have the correct position.
Use flyTo to have a smooth transition to your coordinates. 12 is the zoom level
You don't need to loop over the data since you need only one position. You can replace the for with that.
You're having two problems here:
The response from the Nominatim API is returning several search results, each of them in one GeoJSON Feature inside the response FeatureCollection. It's up to you to choose which search result you want to focus in the map (the first?), or let the user do so.
You're not aware that GeoJSON uses longitude-latitude (or x-y) coordinates, whereas Leaflet uses latitude-longitude (or y-x)

"object reference not set to an instance of an object" when trying to get lat, lng from a KML-layer to Google Maps [Xamarin Android]

I am currently working with a KML-file via this plugin: https://github.com/sushihangover/SushiHangover.Android.Maps.Utils
The KML-file i am using gets added succesfully via this codesnippet:
var kmlLayer = new KmlLayer(googleMap, Resource.Raw.campus, Android.App.Application.Context);
kmlLayer.AddLayerToMap();
MoveCameraToKml(kmlLayer);
When it's added I run my MoveCameraToKmlfunction where I try to get the lat, lng of every point but I get a crash on this row foreach (LatLng latLng in ((KmlLineString)geo).GeometryObject); with the errormessage: object reference not set to an instance of an object
void MoveCameraToKml(KmlLayer kmlLayer)
{
//Retrieve the first container in the KML layer
var container = (KmlContainer)kmlLayer.Containers.Iterator().Next();
//Retrieve a nested container within the first container
container = (KmlContainer)container.Containers.Iterator().Next();
//Retrieve the first placemark in the nested container
var placemark = (KmlPlacemark)container.Placemarks.Iterator().Next();
var geo = placemark.Geometry;
if (geo is KmlLineString)
{
foreach (LatLng latLng in ((KmlLineString)geo).GeometryObject) //object reference not set to an instance of an object
{
System.Diagnostics.Debug.WriteLine(latLng);
}
}
}
Any idea why this is giving me a crash? I am following the example of the nuget I downloaded above.
The idea is to store the lat, lngs in a list and use a PolylineOptions to create routes.
That Google sample (MoveCameraToKml) assumes that you are using their Campus KML example. Since the KML you use will be unique to your app, you will need to review your KML/XML elements and write your code to suit your usage.
Here is an example using their Grand Canyon KML LineString hiking path:
https://developers.google.com/kml/documentation/kml_tut#paths
So looking at the KML, we will need to:
Get the first Container
Get the first Placemark in that container
Check if it has geometry and is a KML LineString
Obtain the LatLng array via GeometryJavaObject()
Use those Latlngs to build a camera viewport and move to it.
"Drive" the camera along those individual points
Grand Canyon KmlLineString Example:
void MoveCameraToKml(KmlLayer kmlLayer)
{
var container = (KmlContainer)kmlLayer.Containers.Iterator().Next();
var placemark = (KmlPlacemark)container.Placemarks.Iterator().Next();
if (placemark.HasGeometry && placemark.Geometry is KmlLineString)
{
var lineString = placemark.Geometry as KmlLineString;
var latlngArray = lineString.GeometryJavaObject() as Java.Util.ArrayList;
using (var builder = new LatLngBounds.Builder())
{
foreach (LatLng latLng in latlngArray.ToEnumerable())
{
builder.Include(latLng);
}
googleMap.MoveCamera(CameraUpdateFactory.NewLatLngBounds(
builder.Build(), mapFragment.View.Width, mapFragment.View.Height, 1)
);
}
Task.Run(async () => // run camera along KmlLineString
{
foreach (LatLng latLng in latlngArray.ToEnumerable())
{
await Task.Delay(2000);
RunOnUiThread(() => googleMap.MoveCamera(CameraUpdateFactory.NewLatLng(latLng)));
}
});
}
}
Geometry can be KmlPoint, KmlLineString, KmlPolygon or a KmlMultiGeometry.
var geo = placemark.Geometry;
if (geo is KmlPolygon) then {
var poly = (KmlPolygon)geo;
}

limiting google maps autocomplete to UK address only

I've been looking at the example on:
http://code.google.com/apis/maps/documentation/javascript/examples/places-autocomplete.html
and have decided to incorporate it into my site.
Is it possible to limit the addresses to UK addresses only?
Try this:
var input = document.getElementById('searchTextField');
var options = {
types: ['(cities)'],
componentRestrictions: {country: 'tr'}//Turkey only
};
var autocomplete = new google.maps.places.Autocomplete(input,options);
You can't strictly/hard limit the locations that it finds, although there is a feature request in the system to do so, but you can set a 'bias' on the results. It's passed in as an argument to the autocomplete method as a google maps bounds object. Autocomplete will then favor locations within those boundaries. Note, however, that since this isn't a hard boundary, if there are matches for the search outside the boundaries it will return those.
From my usage it seems a bit buggy and can use some improvement - especially considering that anything outside your boundary is not tagged by proximity at all, so something one block outside the boundary is just as likely to show as something 1000 miles outside, so make sure you play around with getting the boundaries working right.
You can intercept the JSONP results that are returned by the google.maps.places.Autocomplete functionality and use them as you see fit, such as to limit by country and display the results.
Basically you redefine the appendChild method on the head element, and then monitor the javascript elements that the Google autocomplete code inserts into the DOM for JSONP. As javascript elements are added, you override the JSONP callbacks that Google defines in order to get access to the raw autocomplete data.
It's a bit of a hack, here goes (I'm using jQuery but it's not necessary for this hack to work):
//The head element, where the Google Autocomplete code will insert a tag
//for a javascript file.
var head = $('head')[0];
//The name of the method the Autocomplete code uses to insert the tag.
var method = 'appendChild';
//The method we will be overriding.
var originalMethod = head[method];
head[method] = function () {
if (arguments[0] && arguments[0].src && arguments[0].src.match(/GetPredictions/)) { //Check that the element is a javascript tag being inserted by Google.
var callbackMatchObject = (/callback=([^&]+)&|$/).exec(arguments[0].src); //Regex to extract the name of the callback method that the JSONP will call.
var searchTermMatchObject = (/\?1s([^&]+)&/).exec(arguments[0].src); //Regex to extract the search term that was entered by the user.
var searchTerm = unescape(searchTermMatchObject[1]);
if (callbackMatchObject && searchTermMatchObject) {
var names = callbackMatchObject[1].split('.'); //The JSONP callback method is in the form "abc.def" and each time has a different random name.
var originalCallback = names[0] && names[1] && window[names[0]] && window[names[0]][names[1]]; //Store the original callback method.
if (originalCallback) {
var newCallback = function () { //Define your own JSONP callback
if (arguments[0] && arguments[0][3]) {
var data = arguments[0][4]; //Your autocomplete results
//SUCCESS! - Limit results here and do something with them, such as displaying them in an autocomplete dropdown.
}
}
//Add copy all the attributes of the old callback function to the new callback function. This prevents the autocomplete functionality from throwing an error.
for (name in originalCallback) {
newCallback[name] = originalCallback[name];
}
window[names[0]][names[1]] = newCallback; //Override the JSONP callback
}
}
//Insert the element into the dom, regardless of whether it was being inserted by Google.
return originalMethod.apply(this, arguments);
};
James Alday is correct:
http://code.google.com/apis/maps/documentation/javascript/places.html#places_autocomplete
var defaultBounds = new google.maps.LatLngBounds(
new google.maps.LatLng(49.00, -13.00),
new google.maps.LatLng(60.00, 3.00));
var acOptions = {
bounds: defaultBounds,
types: ['geocode']
};
it is somewhat annoying as searching for Durham gives Durham, North Carolina as the second result, regardless of how you try to persuade it to region bias - you can set it to viewport map bounds and it'll still try to suggest NC state... The jQuery solution can be found here, but doesn't seem to give as many results as the v3 API.
http://code.google.com/p/geo-autocomplete/
The best way you would go about doing this, is to query the places api yourself and appending the queried string with your country. Or, of course, use the geo-autocomplete jQuery plugin.
Just change the google domain for the maps to your country domain and it will automatically search within your country only:
So:
http://maps.google.com/maps/api/geocode/xml?address={0}&sensor=false&language=en
To:
http://maps.google.nl/maps/api/geocode/xml?address={0}&sensor=false&language=nl
Try something like this.
// Change Bangalore, India to your cities boundary.
var bangaloreBounds = new google.maps.LatLngBounds(
new google.maps.LatLng(12.864162, 77.438610),
new google.maps.LatLng(13.139807, 77.711895));
var autocomplete = new google.maps.places.Autocomplete(this, {
bounds: bangaloreBounds,
strictBounds: true,
});
autocomplete.addListener('place_changed', function () {
});
I find that if you set the map to roughly where you want then set bounds to it, the search finds places in that area first. You do not to physically show the map.
It works better than giving random overseas addresses first, setting to country does not work.
The code for autocomplete to get latln is:
<div id="map_canvas"></div>
<input type="text" name="location" id="location" placeholder="Type location...">
<input type="text" name="loc_latitude" id="latitude">
<input type="text" name="loc_longitude" id="longitude">
and the JS is:
$(document).ready(function () {
var mapOptions = {
center: new google.maps.LatLng(52.41041560, -1.5752999),
zoom: 13,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
var map = new google.maps.Map(document.getElementById('map_canvas'),
mapOptions);
var autocomplete;
autocomplete = new google.maps.places.Autocomplete((document.getElementById(searchInput)), {
types: ['geocode'],
});
autocomplete.bindTo('bounds', map);
google.maps.event.addListener(autocomplete, 'place_changed', function () {
var near_place = autocomplete.getPlace();
document.getElementById('latitude').value = near_place.geometry.location.lat();
document.getElementById('longitude').value = near_place.geometry.location.lng();
});
});
$(document).on('change', '#'+searchInput, function () {
document.getElementById('latitude').value = '';
document.getElementById('longitude').value = '';
});
Not exactly what you asked for but it works for me.

Google Maps v3 OverlayView.getProjection()

I cannot seem to figure out why the object returned by getProjection() is undefined. Here is my code:
// Handles the completion of the rectangle
var ne = recBounds.getNorthEast();
var sw = recBounds.getSouthWest();
$("#map_tools_selat").attr( 'value', sw.lat() );
$("#map_tools_nwlat").attr( 'value', ne.lat() );
$("#map_tools_selng").attr( 'value', ne.lng() );
$("#map_tools_nwlng").attr( 'value', sw.lng() );
// Set Zoom Level
$("#map_tools_zoomlevel").attr( 'value', HAR.map.getZoom()+1 );
document.getElementById("map_tools_centerLat").value = HAR.map.getCenter().lat();
document.getElementById("map_tools_centerLong").value = HAR.map.getCenter().lng();
// All this junk below is for getting pixel coordinates for a lat/lng =/
MyOverlay.prototype = new google.maps.OverlayView();
MyOverlay.prototype.onAdd = function() { }
MyOverlay.prototype.onRemove = function() { }
MyOverlay.prototype.draw = function() { }
function MyOverlay(map) { this.setMap(map); }
var overlay = new MyOverlay(HAR.map);
var projection = overlay.getProjection();
// END - all the junk
var p = projection.fromLatLngToContainerPixel(recBounds.getCenter());
alert(p.x+", "+p.y);
My error is: Cannot call method 'fromLatLngToContainerPixel' of undefined
Actually, i the reason why this happens is because the projection object is created after the map is idle after panning / zooming. So, a better solution is to listen on the idle event of the google.maps.Map object, and get a reference to the projection there:
// Create your map and overlay
var map;
MyOverlay.prototype = new google.maps.OverlayView();
MyOverlay.prototype.onAdd = function() { }
MyOverlay.prototype.onRemove = function() { }
MyOverlay.prototype.draw = function() { }
function MyOverlay(map) { this.setMap(map); }
var overlay = new MyOverlay(map);
var projection;
// Wait for idle map
google.maps.event.addListener(map, 'idle', function() {
// Get projection
projection = overlay.getProjection();
})
I kind of figured out what was going on. Even though it is still not crystal clear why this happens, I know that I had to instantiate the variable "overlay" right after instantiating my google map (HAR.map). So I practically moved that code snippet into my HAR class and now i use:
HAR.canvassOverlay.getProjection().fromLatLngToContainerPixel( recBounds.getCenter() );
So now, every time I create a map via my class "HAR" I also have a parallel OverlayView object within my class.
The Error could have been with losing scope of my class object, but I think it was more of the map event "projection_changed" not being fired. I got a hint from the map API docs for map class, under method getProjection():
"Returns the current Projection. If the map is not yet initialized (i.e. the mapType is still null) then the result is null. Listen to projection_changed and check its value to ensure it is not null."
If you are getting the similar issue, make sure that you assign your overlayView.setMAP( YOUR_MAP_OBJECT ) closely after instantiating the map object.