Combining Google Maps MarkerClusterer v3 and Viewport Marker Management - javascript

I've successfully setup both MarkerClusterer v3 and Viewport Marker Management ( performing an ajax call to gather only markers visible with viewport and rendering those whenever the map is 'idle') separately.
However when I combine them, they only seem to work together when the page first loads and not afterward.
When zooming or panning the initial clusters remain and the markers are for the entire map are rendered unclustered, but leaves the previously clustered markers.
The original clustered markers still behave properly though when you zoom in/out, but the new markers provided when the viewport bounds are changed aren't added to them or clustered.
Code Below:
function drawMap(swLat, swLng, neLat, neLng){
//Load the map data
$.ajax({
type: "POST",
url: "readMapInfo.php",
cache: false,
data:{S:swLat, W:swLng, N:neLat, E:neLng},
dataType: "xml",
success: function(data) {
if(markerArray.length >0){
for (i in markerArray) {
markerArray[i].setMap(null);
}
drawMarker(data); //takes the info provided and performs "markerArray.push(marker);"
mc = new MarkerClusterer(map, markerArray, clusterOptions);
} else {
drawMarker(data); //takes the info provided and performs "markerArray.push(marker);"
mc = new MarkerClusterer(map, markerArray, clusterOptions);
}
});
}
google.maps.event.addListener(map, 'idle', function() {
bounds = map.getBounds();
sw = bounds.getSouthWest();
ne = bounds.getNorthEast();
swLat = sw.lat();
swLng = sw.lng();
neLat = ne.lat();
neLng = ne.lng();
drawMap(swLat, swLng, neLat, neLng);
});

Your description of your problem is detailed and thorough, but it would be easier if there were also a URL to a page that demonstrates the problem(s). Of course, that may not be possible in this specific scenario. That said, I will take a crack at helping out:
I believe you need some additional cleanup code at the beginning of your ajax success callback:
if( markerArray.length > 0 ) {
// For loop logic is unchanged
// It removes the markers from the Map, but leaves them in markerArray
for (i in markerArray) {
markerArray[i].setMap( null );
}
// New code to truncate the Array; the markers should be removed
markerArray.length = 0;
// New code to clear the clusterer; the markers should be removed
mc.clearMarkers();
// Original line of code unchanged
drawMarker(data); //takes the data and performs markerArray.push(marker)
// Commented out, because the cluster is cleared each time, not recreated
//mc = new MarkerClusterer(map, markerArray, clusterOptions);
// New code to refill the cluster, rather than recreate the cluster
// The original clusterOptions are retained
mc.addMarkers( markerArray );
} else {
// Original line of code unchanged
drawMarker(data); //takes the data and performs markerArray.push(marker)
// Original line of code unchanged
// Necessary here, because the clusterer does not yet exist
mc = new MarkerClusterer(map, markerArray, clusterOptions);
}
I believe this will help move you forward. Please let me know if this resolves the problem or at least helps.
After you have your immediate challenges resolved, I also suggest that you take a look at MarkerClustererPlus; it is described in the question: Is there any way to disable the Marker Clusterer for less than defined marker counts?.

Related

Google Maps API use zoom level and center position from local storage values to adjust map on page load

I am working with a WordPress/jQuery/Google Maps setup to display a map of listings to users.
The jQuery.goMap.map code is used to load in the Google Maps instance used on the WordPress plugin.
I have wrote the following functions to store and load the latitude, longitude, and zoom level with local storage. The storing of the lat/lng and zoom levels is working, and the loading of the zoom level is working, but I cannot get the map to center on the loaded latitude and longitude position.
I have tried using bounds.extend(latLng); and jQuery.goMap.map.fitBounds(bounds); in the loadUserZoom function, but the result is a fully zoomed in map. This means the stored zoom level value is being ignored.
The current functioning code can be tested here.
The Clear text link in the header navigation can be used to clear the local storage values from the browser. This is implemented for testing purposes.
Any assistance is greatly appreciated.
Function: storeUserZoom
function storeUserZoom() {
let zoom = jQuery.goMap.map.getZoom();
localStorage.setItem( 'zoom', zoom);
let center = jQuery.goMap.map.getCenter();
let lat = center.lat();
let lng = center.lng();
let latLng = {
lat: lat,
lng: lng
}
localStorage.setItem( 'latLng', JSON.stringify(latLng));
}
Function: loadUserZoom
function loadUserZoom() {
if (localStorage.getItem( 'zoom' )) {
let zoom = parseInt(localStorage.getItem( 'zoom' ));
console.log(zoom);
// Logs correct zoom level
let latLng = JSON.parse(localStorage.getItem( 'latLng' ));
console.log(latLng);
// Logs Object { lat: 51.69124213478852, lng: -113.2478200914128 }
jQuery.goMap.map.setZoom(zoom);
jQuery.goMap.map.setCenter(latLng);
// latLng used is incorrect
}
}
I believe I have zeroed in on the problem by adjusting how the loadUserZoom function was executed in the initMap function.
The loadUserZoom function was wrapped in a Google Maps listen once event listener when the map was idle. The code is included below.
google.maps.event.addListenerOnce(jQuery.goMap.map, 'idle', function() {
loadUserZoom();
});
I had it set initially to addListener, which seemed to conflict with the required functionality. I assume this meant it was execute regularly whenever the map was in an idle state.
My updated loadUserZoom function is included below.
function loadUserZoom() {
if (localStorage.getItem( 'zoom' )) {
let zoom = parseInt(localStorage.getItem( 'zoom' ));
let latLng = JSON.parse(localStorage.getItem( 'latLng' ));
jQuery.goMap.map.setZoom(zoom);
jQuery.goMap.map.setCenter(latLng);
}
}

Creating MapKit JS with origin, destination and waypoints

I'm migrating from Google Maps API to Apple MapKit JS for the simple reason I have a developer account with them and they offer more free hits.
Anyway, actual examples of MapKit JS are a bit thin (or at least Google isn't finding them - draw what conspiracy theories you will), so although I've got the basics going of displaying an embeded map, I can't seem to do the next step which is route between two points (Apple's documentation also seems impenetrable as they don't show examples).
Here's my script for a basic map:
<script>
mapkit.init({
authorizationCallback: function(done) {
done('[MY-TOKEN]');
}
});
var MarkerAnnotation = mapkit.MarkerAnnotation
var myMarker = new mapkit.Coordinate(55.9496320, -3.1866360)
var myRegion = new mapkit.CoordinateRegion(
new mapkit.Coordinate(55.9496320, -3.1866360),
new mapkit.CoordinateSpan(0.003, 0.003)
);
var map = new mapkit.Map("map");
var myAnnotation = new MarkerAnnotation(myMarker, { color: "#9b6bcc", title: "theSpace On The Mile"});
map.showItems([myAnnotation]);
map.region = myRegion;
</script>
Now I want to:
• Show a walking route between two points
• Include waypoints on the route
Could someone show the code that would achieve this? Once I can see an example I know I'll get it ;-)
Ok, so I've found a solution to this so sharing it here for the benefit of others.
Let's start by saying Apple's MapKit JS doesn't appear to have a waypoints option as offered by Google Maps API - so the way around that is to create a map that stores the markers in an array and then routes from one to the next. The code stores the location of the last waypoint in a variable, and doesn't bother to draw a route to the last waypoint if this is the first one in the array (obviously).
<script>
// Initiallise MapKit - you'll need your own long-lived token for this
mapkit.init({
authorizationCallback: function(done) {
done('[MY-TOKEN]');
}
});
// Function to draw the route once MapKit has returned a response
function directionHandler(error, data) {
data["routes"].forEach(function(route, routeIdx) {
if (routeIdx !== 0) { return; }
overlays = [];
route['path'].forEach(function(path) {
// This styles the line drawn on the map
let overlayStyle = new mapkit.Style({
lineWidth: 3,
strokeColor: "#9b6bcc"
});
let overlay = new mapkit.PolylineOverlay(path, {
style: overlayStyle
});
overlays.push(overlay);
});
map.addOverlays(overlays);
});
}
// This asks MapKit for directions and when it gets a response sends it to directionHandler
function computeDirections(origin,destination) {
let directionsOptions = {
origin: origin,
destination: destination,
transportType: mapkit.Directions.Transport.Walking
};
directions.route(directionsOptions, directionHandler);
}
// This sets the initial region, but is overridden when all points have been potted to automatically set the bounds
var myRegion = new mapkit.CoordinateRegion(
new mapkit.Coordinate(55.9496320, -3.1866360),
new mapkit.CoordinateSpan(0.05, 0.05)
);
var map = new mapkit.Map("map");
map.region = myRegion;
var myAnnotations = [];
// lastWaypoint variable is 'unset' initially so the map doesn't try and find a route to the lastWaypoint for the first point of the route
var lastWaypoint = "unset";
var directions = new mapkit.Directions();
// Array of co-ordinates and label for marker
waypoints = [
{name:'Sofi’s Bar',lat:55.9746308,lon:-3.1722282},
{name:'TThe Roseleaf Cafe',lat:55.975992,lon:-3.173474},
{name:'Hemingway’s',lat:55.9763631,lon:-3.1706564},
{name:'Teuchter’s Landing',lat:55.9774693,lon:-3.1713826},
{name:'The King’s Wark',lat:55.9761425,lon:-3.1695419},
{name:'Malt and Hops',lat:55.975885,lon:-3.1698957},
{name:'The Carrier’s Quarters',lat:55.9760813,lon:-3.1685323},
{name:'Noble’s',lat:55.974905,lon:-3.16714},
{name:'The Fly Half',lat:55.9747906,lon:-3.1674496},
{name:'Port O’ Leith',lat:55.974596,lon:-3.167525}
];
// Loop through the array and create marker for each
waypoints.forEach(function(data) {
var myAnnotation = new mapkit.MarkerAnnotation(new mapkit.Coordinate(data['lat'],data['lon']), {
color: "#9b6bcc",
title: data['name']
});
myAnnotations.push(myAnnotation);
// As long as this isn't the first point on the route, draw a route back to the last point
if(lastWaypoint!="unset") {
computeDirections(lastWaypoint,new mapkit.Coordinate(data['lat'],data['lon']));
}
lastWaypoint = new mapkit.Coordinate(data['lat'],data['lon']);
});
map.showItems(myAnnotations);
</script>
This map is for a pub crawl around Leith, so the trasportType is 'Walking', but change that to 'Automobile' if you so wish.
With credit to Vasile whose MapKit JS Demo (https://github.com/vasile/mapkit-js-demo) helped me understand a lot more about the options.

Showing an infoWindow immediately after a Layer query succeeded

I have a web application, where a user can switch between some 160-ish layers. Most of them are Feature Layers, but some are of type ArcGISDynamicMapServiceLayer.
I need to be able to query those layers the same as I do with FeatureLayers: by clicking on any point on the map and displaying an infowindow.
This is my code so far (removed some bits for clarity):
executeQueryTask: function(evt, scope) {
//"this" is the map object in this context, so we pass in the scope from the caller,
//which will enable us to call the layer and map object and all the other precious widget properties
scope.map.graphics.clear();
scope.map.infoWindow.hide();
//we create a new Circle and set its center at the mappoint. The radius will be 20 meters
//default unit is meters.
var circle = new Circle({
/*...*/
});
// draw the circle to the map:
var circleGraphic = new Graphic(circle, /*...*/));
scope.map.graphics.add(circleGraphic);
var queryTask = new QueryTask(scope.layer.layer.url + "/" + scope.layer.layer.visibleLayers[0]);
var query = new Query();
query.returnGeometry = true;
query.outFields = ["*"];
query.geometry = circle.getExtent();
var infoTemplate = new InfoTemplate().setTitle("");
queryTask.execute(query, function(resultSet) {
array.forEach(resultSet.features, function(feature) {
var graphic = feature;
graphic.setSymbol(/*...*/));
//Set the infoTemplate.
// graphic.setInfoTemplate(infoTemplate);
//Add graphic to the map graphics layer.
scope.map.infoWindow.setContent(graphic.attributes);
scope.map.infoWindow.show(evt.mapPoint, scope.map.getInfoWindowAnchor(evt.screenPoint));
scope.map.graphics.add(graphic);
});
});
},
The key point is insise the queryTask.execute callback. If I uncomment and use graphic.setInfoTemplate(infoTemplate); the result is colored and upon a second click an infoWindow pops up.
There are 2 issues with this approach:
2 clicks are needed
I am unable to click on PolyLines and Points twice, so no infowindow pops up here.
This is why I added a circle to get a 100m buffer in radius to my click. Now I want to immediatly return an infoWindow.
At this point I'm struggeling to successfully create an Info Window, which is immediately displayed.
Currently the line scope.map.infoWindow.setContent(graphic.attributes); throws an error Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node'.
How can I create that Info Window?
I found a suitable approach, which leaves room for improvements. But this is for another iteration.
//create a new FeatureLayer object
var featureLayer = new FeatureLayer(scope.layer.layer.url + "/" + scope.layer.layer.visibleLayers[0], {
mode: FeatureLayer.MODE_SELECTION,
infoTemplate: new InfoTemplate("Attributes", "${*}"),
outFields: ["*"]
});
//we create a new Circle and set its center at the mappoint. The radius will be 20 meters
//default unit is meters.
var circle = new Circle({/*...*/});
// draw the circle to the map:
var circleGraphic = new Graphic(circle, /*...*/));
scope.map.graphics.add(circleGraphic);
var lQuery = new Query();
lQuery.returnGeometry = true;
lQuery.geometry = circle.getExtent();
featureLayer.queryFeatures(lQuery, function(results) {
array.forEach(results.features, function(feature) {
var graphic = feature;
graphic.setSymbol(/*...*/));
//now that we have the feature, we need to select it
var selectionQuery = new Query();
selectionQuery.geometry = feature.geometry;
featureLayer.selectFeatures(selectionQuery, FeatureLayer.SELECTION_NEW)
.then(function(selectedFeatures) {
console.info("selection complete", selectedFeatures);
if (!selectedFeatures.length) {
return;
}
scope.map.infoWindow.setFeatures(selectedFeatures);
scope.map.infoWindow.show(evt.mapPoint, "upperright");
});
});
});
The change here is, that we are no longer using a QueryTask, but create a new FeatureLayer object in selection mode, using the url and id of the visible layer.
The second noteworthy change is, that we no longer set the content of the infoWindow, but instead set selected features using infoWindow.setFeatures(selectedFeatures). Setting the content of an infoWindow, but not selecting features, hides the action list of the info window, this hinders you to zoom to an object or perform other custom operations.
In addition, this enables you( or me ) to view multiple results in the infoWindow, instead of just one.

Converting JSON values to an array

I working with leaflet maps and html5 geolocation. I have a view that receives coords from a node.js server and adds markers for each unique id and updates their positions as new coords come in. I'm storing the marker objects in an object, the IDs as the keys, the markers as the values. So the markers object looks like this when tracking two devices:
Object {qwpW9K0L1OtOhsV3CF0G: e, syeQs_oH7s8z8f0UCF0I: e}
Where the e is an actual marker.
The problem I'm working on is that the leaflet api likes to receive arrays of data. For example, I'd like to fit the map view bounds to the markers using a feature group and map.fitBounds(...). So i'm iterating through the object and pushing the markers into a new array and updating the map bounds every time a new marker is created or new coords come in and it seems extremely inefficient and just plain the wrong way to do it.
Complete socket.io code block I'm working on:
socket.on('markerCoords', function(data) {
var markerGroup = [];
if(data.id in markers) {
markers[data.id].setLatLng([data.data.latitude, data.data.longitude]);
$.each(markers, function(i, e) {
markerGroup.push(e);
});
var group = new L.featureGroup(markerGroup);
map.fitBounds(group.getBounds().pad(0.5));
} else {
var marker = L.marker([data.data.latitude, data.data.longitude], {"title": data.id, "draggable": true}).addTo(map);
markers[data.id] = marker;
$.each(markers, function(i, e) {
markerGroup.push(e);
});
var group = new L.featureGroup(markerGroup);
map.fitBounds(group.getBounds().pad(1.0));
}
});
Looks like one possible solution is to simply create the feature group only once and updated it when necessary:
var group = new L.featureGroup();
socket.on('markerCoords', function(data) {
var markerGroup = [];
if(data.id in markers) {
markers[data.id].setLatLng([data.data.latitude, data.data.longitude]);
map.fitBounds(group.getBounds().pad(0.5));
} else {
markers[data.id] = L.marker(
[data.data.latitude, data.data.longitude],
{"title": data.id, "draggable": true}
).addTo(map);
group.addLayer(markers[data.id]);
map.fitBounds(group.getBounds().pad(1.0));
}
});
You could probably also just add the group to the map once on startup, not every marker individually.

Google Maps v3 -> Database/mysql -> AJAX -> LOOP -> AJAX -> Loop

a) the real joke first:
Finally buying Microsoft stuff over the years paid off..
Trying to debug using Chrome, my map was showing just one marker. It was on the correct latlng for the period of the setTimeout, blinks for a small fraction of second, back to same position.
At a certain time, shot code to Explorer by mistake....... voila... code iterates through the database, but does not use the setTimeout for each marker. At Explorer, setTime out (Ex, 5 seconds), means all the markers will show up, with the right infowindow, but it's 5 seconds for all markers. The longer the time, the longer it will stay in one of them (alwyas the same one), going really fast on the other markers. At chrome, the iteration was so fast I could not see the iteration thru the other latlng's. It was just a blink.
b) I guess the problem is that the getJson (or Ajax), using either for loop or $.each (I used all sorts of combinations.....) is combined with the other loop inside the function changeMarker. So there are two loops going on at the same time. However, I don't know how to fix it. If I close the Ajax (or getJson) right after the $.each or for loop, the rest of the code doesn't get the values. Nothing happens (just my alert, which is for debugging purposes).
No, I don't fully understand closures. Yes, I read a bunch of stuff, the main one here, but also here and here and there. but still didn't figure it out to :/
c) Not easy being a newbie, trying to solve a problem for days... and not getting it solved.
Any help will be greatly appreciated!
Here is the code - omitted a long section where map gets personal options.
var BERLIN = new google.maps.LatLng(-32.517683, -46.394393);
var map = null;
var marker = null;
var index = 0;
var infoWindow = null;
var latlng ;
var MY_MAPTYPE_ID = 'custom_style';
function initialize() {
//personal options not included here.
var customMapType = new google.maps.StyledMapType(featureOpts, styledMapOptions);
map.mapTypes.set(MY_MAPTYPE_ID, customMapType);
$.getJSON('php/locationsJson.php',function(json){
$.each( json, function(i, item) {
var lat = this.lat;
var lng = this.lng;
var location = new google.maps.LatLng(json[i].lat,json[i].lng);
alert( json[i].lat +','+json[i].lng );
function dropMarker (map, pos){
return new google.maps.Marker({
map: map,
position: location,
draggable: false,
}); // return
}
function changeMarker() {
if (marker) {
infoWindow.close();
marker.setMap(null);
}
var pos = location[index];
marker = dropMarker(map,pos);
var contentString = ('lat: ' + location.lat() + '<br />' + 'lng: ' + location.lng())
infoWindow.setContent(contentString);
setTimeout(function () {
infoWindow.open(map, marker);
}, 100);
index = (index + 1) % json.length;
setTimeout(function () {
changeMarker();
}, 4000);
}
var customMapType = new google.maps.StyledMapType(featureOpts, styledMapOptions);
infoWindow = new google.maps.InfoWindow()
changeMarker();
}); //$.each
}); //end of getjson
} //end of initialized
Here is fiddle (thanks for the help to get to that point) for the code BEFORE the AJAX. I tried to add the json file in the fiddle, but it's too complex to add json inside the fiddle.
Thank you again for your time.
As your approach in the fiddle works fine, you don't need much modifications to
achieve it via ajax.
Instead of starting the timeouts inside the loop use the loop only to populate the NEIGBORHOODS-array, and after the loop call changeMarker()
The success-callback for $.getJSON:
function(json){
NEIGBORHOODS=[];
$.each(json,
function(){
NEIGBORHOODS.push(new google.maps.LatLng(this.lat,this.lng));
});
changeMarker();
}
The rest of the code may stay as it is in the fiddle.
Working fiddle with ajax-request: http://jsfiddle.net/doktormolle/CVECG/
(Note: In the fiddle I've used $.post() because jsfiddle requires post-requests for JSON, you can use $.getJSON in your application)

Categories

Resources