Leaflet Markercluster - write persistent data to each marker - javascript

I have a map implemented via leaflet and there is a button to send the bounds of the map to a server which in turn responds to me with an array of elements. Each element has a latitude, a longitude and a unique id.
What I get from the server looks roughly like this:
[{
"poi_marker": "redMarker.png",
"poi_id": 412, //this is my unique ID
"poi_pos": {
"lat": 17.53243,
"lng": 14.52353
}
},...]
So far my code looked like this:
function createAndAddMarker(prefill) {
let marker = createMarker(prefill.poi_pos, prefill.poi_marker);
map.addLayer(marker);
marker._icon.dataset.uniqueId = prefill.poi_id;
return marker;
}
function createMarker(coords, iconUrl) {
let redMarker = L.icon({
iconUrl: iconUrl,
iconSize: [20, 32], // size of the icon
iconAnchor: [10, 32], // point of the icon which will correspond to marker's location
});
let createdMarker = new L.marker(coords, {
draggable: true,
zIndexOffset: 101,
icon: redMarker
});
return createdMarker;
}
and that worked fine! Now that I tried to implement leaflet.markercluster the first function looks pretty similar:
function createAndAddMarker(prefill) {
let marker = createMarker(prefill.poi_pos, prefill.poi_marker);
clusterLayer.addLayer(marker); //clusterLayer = layer that got created & added a when map did
marker._icon.dataset.uniqueId = prefill.poi_id;
return marker;
}
but there is already 2 problems with it.
Not all the markers get actually drawn to the map because some are already clustered. For these items that don't get drawn, the line marker._icon.dataset.uniqueId = prefill.poi_id will throw an error as it can not find "marker._icon" because the marker doesn't seem to have an icon as long as it's not drawn.
The markers that get drawn get the uniqueId written onto them, but if I zoom out and make them cluster up too, the original marker+icon gets removed from the DOM and readded when I zoom in again. Unfortunately, it get's readded without my unique-id, so I have no idea how to persistenly link the marker with the unique Id.
I need to get corresponding unique-ID to a marker, every time I click on it, but have no idea how to achieve this.
Your input is very much appreciated!
If something is unclear pls let me know and I will attempt to clarify as much as possible.
Thanx in advance!

As you have very well spotted, the Leaflet Marker icon may be removed from DOM / recreated from scratch when Leaflet.markercluster plugin needs to hide the Marker (e.g. if it is clustered or far away from the view port).
Hence attaching data to that icon is troublesome.
But you have several other possibilities to attach your data to the Marker instead, as described in Leaflet: Including metadata with CircleMarkers:
directly to the Marker object
within the options when creating the Marker
to the Marker Feature properties
You do not necessarily need to use a DOM object's dataset to attach your JavaScript data.

Related

I have a location list of 300000 elements and want to visualize them on my localhost

Basically, what I have is a geolocation data (longtitude and latitudes) of 300.000 locations. I have different attributes attached to the data and it is approx. 32MB. Reading it through js and putting markers on google maps is what I've tried, and It works OK when i put only 25 to 2500 markers on my map, I cant really put all of my locations at once. Eventually I want to be able to filter markers through the attributes etc. The locations are all at one city, so I might use my own map or something.
What I want to ask/learn is do you have any better solutions for this particular situation?
Here is my code.
function initJS() {
var promise = getData();
var locations1;
var locations;
promise.success( (data) => {
locations1 = parseData(data);
locations = locations1.filter(location => {
return location.DuyuruTipi == "16";
});
//initializing google map
map = new google.maps.Map(document.getElementById("map"), {
center: { lat: latitude, lng:longtitude},
zoom: zooming,
});
//creating a marker
const svgMarker = {
// path: "M10.453 14.016l6.563-6.609-1.406-1.406-5.156 5.203-2.063-2.109-1.406 1.406zM12 2.016q2.906 0 4.945 2.039t2.039 4.945q0 1.453-0.727 3.328t-1.758 3.516-2.039 3.070-1.711 2.273l-0.75 0.797q-0.281-0.328-0.75-0.867t-1.688-2.156-2.133-3.141-1.664-3.445-0.75-3.375q0-2.906 2.039-4.945t4.945-2.039z",
url: "./assets/marker.svg",
size: new google.maps.Size(20,20),
scaledSize: new google.maps.Size(20,20),
origin: new google.maps.Point(0, 0),
anchor: new google.maps.Point(10, 10),
};
for (var i = 0; i < locations.length; i++) { //locations.length
// init markers
var marker = new google.maps.Marker({
position: { lat: parseFloat(locations[i]["YCoor"]), lng: parseFloat(locations[i]["XCoor"])},
map: map,
label: locations[i]["DuyuruId"],
icon: svgMarker
});
console.log(locations[i]["DuyuruTipi"]);
marker.duyurutipi = locations[i]["DuyuruTipi"];
marker.kazatipi = locations[i]["KazaTipi"];
marker.vsegid = locations[i]["vSegID"];
markers.push(marker);
}
});
Displaying 300000 points directly on the map is not the correct approach and will not perform well, especially as more datasets get added to your map.
In addition, sending 32MB of data or more to the browser is bad form, even for a web map application. If you try out e.g. Google Maps with the network panel open, you'll see that you'll barely go over a few MB even after quite some time using it.
There are a couple approaches that web mappers take to counter this:
Use a service such as Geoserver or Mapserver to split up the data into "chunks" based on what is in the the map clients (openlayers, in your case, according to your answer) viewport. This is the best choice if you could potentially have lots of layers or basemaps in future, but is a lot of work to setup and configure.
Write your own implementation of the above in your back-end. For points, this is relatively simple. This is the best choice for something quick with just a couple of points layers.
In both cases, you'll need to configure your points layer in OpenLayers to use the "bbox" strategy, which will tell it to call your API url whenever the viewport changes enough for more features to be loaded. You will also need to set the minimum resolution for your layer so that it doesn't load too many features all at once when zoomed out.
Lastly, with Openlayers, you'll want to use a VectorImageLayer with a VectorSource for this layer, which will improve performance a lot while allowing you to query and edit your point data.
The above should help to improve your mapping performance.
Well, I went with the OpenLayers API, I think it is harder to implement stuff from docs but they have example applications for every feature. You might want to try that, way better performance if your only need is to put some markers and visualize data.

How to open popup for a specific marker inside a leaflet MarkerClusterGroup?

I want to open a popup for a marker that is under a markercluster when the map is zoomed out. This function is called when a user clicks on a search result.
This is the code that I am using:
map.eachLayer(function (layer) {
if (layer.options && layer.options.pane === "markerPane") {
if (layer.options.title == locationId) {
layer.openPopup()
}
}
});
I tried adding this code but it didn't work as well:
layer.zoomToBounds({padding: [20, 20]});
So you want to so something about a cluster's marker whenever a specific marker in such cluster fulfills a condition.
You can iterate through all visible cluster markers, then leverage getAllChildMarkers; but that will get messy soon, as you will have to deal with the fact that a cluster and the cluster's marker are different entities, so iterating through visible markers doesn't necessarily mean iterating through the visible clusters.
I suggest an approach based on getVisibleParent. Store a reference to each original marker, indexed by the ID you'll be using later for lookup, e.g. ...
var clusterGroup = L.markerClusterGroup();
var markers = {}; // Yay using Object as a hashmap!
for (var i in dataset) {
// Create individual marker based on a item in the dataset, e.g.
var marker = L.marker(dataset[i].latlng);
// Add that to the clusterGroup (but not to the map)
clusterGroup.addMarker(marker);
// Save the individual marker in the hashmap, indexed by the
// desired property, e.g. "locationId"
markers[ dataset[i].locationId ] = marker;
}
// Adding the cluster to the map after all items have been inserted should
// be slightly more performant than doing that before.
clusterGroup.addTo(map);
So with that, one should be able to look up the marker by the desired ID, see if it's in a cluster or directly visible, and do something about it:
function highlightLocationId(id) {
// hashmap lookup
var marker = markers[i];
// Sanity check
if (!marker) { return; }
// What cluster is this marker in?
var cluster = clusterGroup.getVisibleParent(marker);
// Is the marker really in a cluster, or visible standalone?
if (cluster) {
// It's in a cluster, do something about its cluster.
cluster.openPopup();
} else {
// It's not in a cluster but directly in the map, do something about it.
marker.openPopup();
}
}
I found a fix for this issue. Before trying to open the popup in the cluster I teleport to the coordinates of the location. Then the cluster will automatically open up.
map.setView([coordinates[1], coordinates[0]], 20);
I define which coordinates should be used and what the zoom level should be. After this function I use the layer.openPopup() function to open the popup.

How to modify or remove some existing data in GeoJSON Leaflet object?

Recently I asked about referencing the data of an existing GeoJSON Leaflet object. My Leaflet map consists of data coming in a stream to my GeoJSON Leaflet object. User inputs can change a filter for the GeoJSON data, so to make the filter apply to both the existing and new data I am keeping track of my data in an array called myFeatures. Whenever the filters change or an item in myFeatures changes, I do the following:
myGeoJson.clearLayers();
myGeoJson.addData(myFeatures);
This is working to make my map update according to the newly updated feature data or the changes in the filter.
I am applying pop-ups to the GeoJSON object when I initialize my GeoJSON object:
var myGeoJson = L.geoJson(myFeatures, {
style: function(feature) {
...
},
pointToLayer: function(feature, latlng) {
return L.circleMarker(latlng, geojsonMarkerOptions);
},
filter: function(feature, layer) {
...
},
onEachFeature: function(feature, layer) {
if (feature.properties && feature.properties.popupContent) {
layer.bindPopup(feature.properties.popupContent);
}
}
});
When I click on an individual feature, the pop-up appears. However, the pop-up dismisses pretty quickly, thanks to clearLayers and addData being called. :(
Is there some kind of way to stop the pop-up dismissing in this situation?
Or - better question - is there a way to modifying existing data in a GeoJSON object or remove some (not all) data from a GeoJSON object?
To provide some context, my GeoJSON shows circle markers for each feature. The circle markers are colored based on a property of the feature. The property can actually change over time, so the marker's styling needs to be updated. A marker also times out after a while and needs to be removed from the map, but the other markers need to stay on the map.
There are for sure better ways to do that, but if you don't want to modify your code architecture too much, you could just create your popups in a specific layer, which you won't clear when you add your new data.
To give you an idea (markers play below the role of myGeoJson in your example):
var popup_id = {};
var popup_layer = new L.layerGroup();
var markers = new L.layerGroup();
$.each(testData, function(index, p) {
var marker = L.marker(L.latLng(p.lat, p.lon));
markers.addLayer(marker);
popup = new L.popup({offset: new L.Point(0, -30)});
popup.setLatLng(L.latLng(p.lat, p.lon));
popup.setContent(p.text);
popup_id[p.id] = popup;
marker.on('click', function() {
popup_id[p.id].openPopup();
popup_layer.addLayer(popup_id[p.id]);
markers.clearLayers();
})
});
popup_layer.addTo(map);
markers.addTo(map);
You also keep track of all your popups in a dictionary popup_id.
Since you haven't provided us with a JSfiddle it is a bit difficult to find the perfect answer for your case, but I hope that the popup layer (also here in my fiddle) gives you a good direction.

Leaflet.draw mapping: How to initiate the draw function without toolbar?

For anyone experienced with leaflet or leaflet.draw plugin:
I want to initiate drawing a polygon without using the toolbar from leaflet.draw. I've managed to find the property that allows editing without using the toolbar (layer.editing.enable();) by searching online (it's not in the main documentation). I can't seem to find how to begin drawing a polygon without the toolbar button. Any help would be much appreciated!
Thank you :)
This simple code works for me:
new L.Draw.Polyline(map, drawControl.options.polyline).enable();
Just put it into the onclick handler of your custom button (or wherever you want).
The variables map and drawControl are references to your leaflet map and draw control.
Diving into the source code (leaflet.draw-src.js) you can find the functions to draw the other elements and to edit or delete them.
new L.Draw.Polygon(map, drawControl.options.polygon).enable()
new L.Draw.Rectangle(map, drawControl.options.rectangle).enable()
new L.Draw.Circle(map, drawControl.options.circle).enable()
new L.Draw.Marker(map, drawControl.options.marker).enable()
new L.EditToolbar.Edit(map, {
featureGroup: drawControl.options.featureGroup,
selectedPathOptions: drawControl.options.edit.selectedPathOptions
})
new L.EditToolbar.Delete(map, {
featureGroup: drawControl.options.featureGroup
})
I hope this will be useful for you too.
EDIT:
The L.EditToolbar.Edit and L.EditToolbar.Delete classes expose the following useful methods:
enable(): to start edit/delete mode
disable(): to return to standard mode
save(): to save changes (it fires draw:edited / draw:deleted events)
revertLayers(): to undo changes
I think it's worth mentioning Jacob Toyes answer to this problem. You're always drawing with handlers in leaflet.draw - not directly with layers. If you want to edit a layer, you use the handler saved in a layers editing field like that: layer.editing.enable();. And if you want to create a new layer, you first create a new handler:
// Define you draw handler somewhere where you click handler can access it. N.B. pass any draw options into the handler
var polygonDrawer = new L.Draw.Polyline(map);
// Assumming you have a Leaflet map accessible
map.on('draw:created', function (e) {
var type = e.layerType,
layer = e.layer;
// Do whatever you want with the layer.
// e.type will be the type of layer that has been draw (polyline, marker, polygon, rectangle, circle)
// E.g. add it to the map
layer.addTo(map);
});
// Click handler for you button to start drawing polygons
$('#draw_poly').click(function() {
polygonDrawer.enable();
});
By now there actually is an example on the leaflet.draw github page: https://github.com/Leaflet/Leaflet.draw/blob/develop/docs/examples/edithandlers.html
Nevertheless I think handlers aren't well documented there yet.
Like stated above, L.EditToolbar.Edit and L.EditToolbar.Delete expose interesting methods and events like editstart and editstop. What's not mentioned is that these two classes themselves are derived from L.Handler.
So I've figured this out for circles, but it should be the same for polygons. It's actually really simple. Hopefully the following code answers your question, but if not let me know and I can post more to a gist or something.
// Creates the circle on the map for the given latLng and Radius
// If the createdWithAddress flag is true, the circle will not update
// it's address according to its position.
createCircle: function(latLng, radius, createdWithAddress) {
if (!this.circle) {
var self = this,
centerIcon,
centerMarker;
centerIcon = new L.Icon({
iconUrl: '/assets/location_pin_24px.png',
iconSize: [24, 24],
iconAnchor: [12, 24],
shadowUrl: '/assets/marker-shadow.png',
shadowSize: [20, 20],
shadowAnchor:[6, 20]
})
// Setup the options for the circle -> Override icons, immediately editable
options = {
stroke: true,
color: '#333333',
opacity: 1.0,
weight: 4,
fillColor: '#FFFFFF',
moveIcon: centerIcon,
resizeIcon: new L.Icon({
iconUrl: '/assets/radius_handle_18px.png',
iconSize: [12, 12],
iconAnchor: [0,0]
})
}
if (someConfigVarYouDontNeedToKnow) {
options.editable = false
centerMarker = new L.Marker(latLng, { icon:centerIcon })
} else {
options.editable = true
}
// Create our location circle
// NOTE: I believe I had to modify Leaflet or Leaflet.draw to allow for passing in
// options, but you can make it editable with circle.editing.enable()
this.circle = L.circle([latLng.lat, latLng.lng], radius, options)
// Add event handlers to update the location
this.circle.on('add', function() {
if (!createdWithAddress) {
self.reverseGeocode(this.getLatLng())
}
self.updateCircleLocation(this.getLatLng(), this.getRadius())
self.updateMapView()
})
this.circle.on('edit', function() {
if (self.convertLatLngToString(this.getLatLng()) !== self.getLocationLatLng()) {
self.reverseGeocode(this.getLatLng())
}
self.updateCircleLocation(this.getLatLng(), this.getRadius())
self.updateMapView()
})
this.map.addLayer(this.circle)
if (centerMarker) {
centerMarker.addTo(this.map)
this.circle.redraw()
centerMarker.update()
}
}
},
Sorry a lot of that is just noise, but it should give you an idea of how to go about this. You can control editing like you said with editing.enable()/.disable().
Make sure to comment with any questions. Good luck man.
While BaCH's solution is propably the best, I would like to add a one-liner solution which is actually more future-proof (than digging into undocumented Leaflet Draw methods) and the simplest.
document.querySelector('.leaflet-draw-draw-polygon').click();
That's it.
You just take andvantage of the existance of the toolbar but actually, you do not use it. You can initiate drawing programmatically in any way. And you can also hide the toolbar using CSS.
In case you want to initiate drawing of a different shape, use one of the following classes:
.leaflet-draw-draw-polyline
.leaflet-draw-draw-rectangle
.leaflet-draw-draw-circle
.leaflet-draw-draw-marker
.leaflet-draw-draw-circlemarker

Creating a map using only toggleable overlays?

I'm trying to create map (using the Google Maps JavaScript API V3) which consists of several partially-transparent layers. By default, these layers should all be overlaid on top of one another to form a complete map, but the user should be able to turn any combination of them on or off (while preserving order) to create whatever view they prefer.
So far, I've had a great deal of luck getting this working for a single layer using map.mapTypes, but when adding all the layers via map.overlayMapTypes, I've hit a couple of snags:
The map doesn't seem to get fully initialized if map.setMapTypeId() is not called (no controls appear and the map is not correctly centered) and it cannot be called with an overlay.
It isn't clear how to toggle the visibility of an overlay without directly modifying the map.overlayMapTypes array, which complicates keeping them correctly ordered. I'd much prefer something analogous to the Traffic/Transit/Photos/etc. control available within Google Maps itself.
Here's the initialize function I'm working with. I'd post a link, but the map imagery isn't publicly available:
function initialize() {
map = new google.maps.Map(document.getElementById("map_canvas"), {
zoom: 0,
center: center
});
/* if these lines are uncommented, the single layer displays perfectly */
//map.mapTypes.set("Layer 3", layers[3]);
//map.setMapTypeId("Layer 3");
//return;
var dummy = new google.maps.ImageMapType({
name: "Dummy",
minZoom: 0,
maxZoom: 6,
tileSize: new google.maps.Size(256, 256),
getTileUrl: function() {return null; }
});
map.mapTypes.set("Dummy", dummy);
map.setMapTypeId("Dummy");
// layers is an array of ImageMapTypes
for (var i = 0; i < layers.length; i++) {
map.overlayMapTypes.push(layers[i]);
}
}
As you can see, I've tried creating a "dummy" maptype (which always returns null for tile URLs) to serve as the base map. While this does cause the controls to display, it still doesn't center correctly.
What's the best way to create a map which consists only of toggleable overlays?
Update: Turns out the dummy maptype works perfectly well if you also remember to set a projection. That's one problem solved, at least. :-)
I use ImageMapType, but I don't add it to mapTypes. I just add it to overlayMapTypes and when I need to remove it I use setAt to set the entry in overlayMapTypes to null.
You will need to add individual controls to the UI that toggle the individual layers.

Categories