I am using several L.markerClusterGroup({})'s so that I can switch them in a Layers Control.
But the Clusters hide behind each other.
I would like to be able to get the total number of both Clusters.
What am I missing?
The issue is that each Leaflet Marker Cluster Group (i.e. L.markerClusterGroup) will perform its own clustering and render Clusters irrespective of what other Cluster Groups may display. Therefore if you have some individual Markers (or any point Features) that are in different Cluster Groups but close to each other, these Groups will display Clusters also close to each other, which may end up overlapping, especially at low zoom level, exactly like in your screenshot.
If you want your Markers to cluster all together (mixing "oranges with apples"), you should use a single Marker Cluster Group.
Now if I understand correctly, your "difficulty" is that you would like to be able to add and remove the Markers dynamically, i.e. in your case the user can use a Layers Control to switch on/off some Features on map.
In that case, you would probably be interested in Leaflet.FeatureGroup.SubGroup plugin (see demo). Simply create 1 subgroup per "switchable" Features Group and set their parent as your single Marker Cluster Group:
var map = L.map('map', {
maxZoom: 18,
}).setView([48.86, 2.35], 11);
var parentGroup = L.markerClusterGroup().addTo(map);
var overlays = {};
for (var i = 1; i <= 5; i += 1) {
overlays['Group ' + i] = L.featureGroup.subGroup(
parentGroup,
getArrayOfMarkers()
).addTo(map);
}
L.control.layers(null, overlays, {
collapsed: false,
}).addTo(map);
function getArrayOfMarkers() {
var result = [];
for (var i = 0; i < 10; i += 1) {
result.push(L.marker(getRandomLatLng()));
}
return result;
}
function getRandomLatLng() {
return [
48.8 + 0.1 * Math.random(),
2.25 + 0.2 * Math.random()
];
}
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
html,
body,
#map {
height: 100%;
margin: 0;
}
<!-- Leaflet assets -->
<link rel="stylesheet" href="https://unpkg.com/leaflet#1.3.3/dist/leaflet.css" integrity="sha512-Rksm5RenBEKSKFjgI3a41vrjkw4EVPlJ3+OiI65vTjIdo9brlAacEuKOiQ5OFh7cOI1bkDwLqdLw3Zg0cRJAAQ==" crossorigin="" />
<script src="https://unpkg.com/leaflet#1.3.3/dist/leaflet.js" integrity="sha512-tAGcCfR4Sc5ZP5ZoVz0quoZDYX5aCtEm/eu1KhSLj2c9eFrylXZknQYmxUssFaVJKvvc0dJQixhGjG2yXWiV9Q==" crossorigin=""></script>
<!-- Leaflet.markercluster assets -->
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster#1.3.0/dist/MarkerCluster.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster#1.3.0/dist/MarkerCluster.Default.css">
<script src="https://unpkg.com/leaflet.markercluster#1.3.0/dist/leaflet.markercluster-src.js"></script>
<!-- Leaflet.FeatureGroup.SubGroup assets -->
<script src="https://unpkg.com/leaflet.featuregroup.subgroup#1.0.2/dist/leaflet.featuregroup.subgroup.js"></script>
<div id="map"></div>
See also Cluster multiple Layers with markercluster
Note: for more complex use cases, there is also another plugin Leaflet.MarkerCluster.LayerSupport.
See also How to apply leaflet marker cluster using layers
Disclosure: I am the author of these plugins.
Related
Trying to retrieve part of a district, however for some reason cannot see the whole area, even if zoom level is at 0, where (supposedly) we should see the whole world.
I am using L.CRS.Simple because this uses the EPSG:3763 and cannot see that one on the CRS list. I am retrieving the data in JSON cause when tying with geoJSON, was not able to transform the 3D coordinates data into 2D planes ones.
const queryRegionText = "where=OBJECTID > 0"
const geoJsonURL2 = "https://sig.cm-figfoz.pt/arcgis/rest/services/Internet/MunisigWeb_DadosContexto/MapServer/2/query?f=json&returnGeometry=true&geometryType=esriGeometryPolyline&spatialRel=esriSpatialRelIntersects&outFields=*&outSR=3763&" + queryRegionText
var map = L.map('mapid', {
crs: L.CRS.Simple
}).setView([-58216.458338, 42768.347232], 0);
L.control.scale({ metric: true }).addTo(map);
fetch(geoJsonURL2).then(function (response) {
response.json().then(function (data) {
data.features.forEach(element => {
if (element.geometry.rings) {
element.geometry.rings.forEach(point => {
L.polyline(point, { color: 'red' }).addTo(map);
})
}
});
});
});
var popup = L.popup();
function onMapClick(e) {
popup
.setLatLng(e.latlng)
.setContent("You clicked the map at " + e.latlng.toString())
.openOn(map);
}
map.on('click', onMapClick);
<html>
<head>
<title>Leaflet - testing</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="https://unpkg.com/leaflet#1.7.1/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet#1.7.1/dist/leaflet.js"></script>
</head>
<body>
<div id="mapid" style="width: 600px; height: 400px;"></div>
</body>
</html>
TL;DR: When creating the map, set the minimum zoom below zero. This should work:
var map = L.map('mapid', {
crs: L.CRS.Simple, minZoom: -6
}).setView([-57728, 55296], -6);
Explanation
Normally, Leaflet translates from a latitude/longitude coordinate system to screen pixels using an assumption that the world is 256 pixels high at Zoom level 0. At each higher Zoom Level, the number of pixels doubles (explained nicely in the Zoom levels tutorial). With this assumption, the options for the map default to {minZoom: 0, maxZoom: Infinity} (as you are not adding any Layer that sets these values to anything different).
When you use L.CRS.Simple, at Zoom level 0 it maps 1 coordinate unit to 1 screen pixel. Your data looks like it is about 18000 coordinate units tall, so it doesn't fit in your 400 pixel high map. To make it fit, we need each screen pixel to map to about 45 coordinate units. 2^5 is 32, and 2^6 is 64, so we need to zoom out between 5 and 6 times. Luckily, Leaflet accepts negative Zoom Levels, so setting zoom to -6 does the trick. But to make it work properly, you need to set {minZoom: -6}, so the map doesn't get stuck at zoom level 0. There's a good worked example in the Non-geographical Maps tutorial.
Using L.CRS.Simple should work for you, so long as the approximation holds that each latitude unit is the same length as each longitude unit (a square world). Since this isn't generally true in the real world, using the Simple projection will cause some distortion. If the distortion is significant for the features you are interested in, then you will need to look up how to use EPSG:3763 properly, using L.CRS and Proj4Leaflet, as suggested by #IvanSanchez.
So, after some reading on the proj4leaflet, come up with this code. Thanks in advance for the comments and the reply above.
const queryRegionText = "where=OBJECTID > 0"
const geoJsonURL2 = "https://sig.cm-figfoz.pt/arcgis/rest/services/Internet/MunisigWeb_DadosContexto/MapServer/2/query?f=geojson&returnGeometry=true&geometryType=esriGeometryPolyline&spatialRel=esriSpatialRelIntersects&outFields=*&outSR=3763&" + queryRegionText
const map = L.map('map', {
center: [40.14791, -8.87009],
zoom: 13
});
proj4.defs("EPSG:3763", "+proj=tmerc +lat_0=39.66825833333333 +lon_0=-8.133108333333334 +k=1 +x_0=0 +y_0=0 +ellps=GRS80 +units=m +no_defs");
fetch(geoJsonURL2).then(function (response) {
response.json().then(function (data) {
L.Proj.geoJson(data).addTo(map);
});
});
var popup = L.popup();
function onMapClick(e) {
popup
.setLatLng(e.latlng)
.setContent("You clicked the map at " + e.latlng.toString())
.openOn(map);
}
map.on('click', onMapClick);
<head>
<link rel="stylesheet" href="https://unpkg.com/leaflet#1.7.1/dist/leaflet.css" />
<link rel="stylesheet" href="main.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.7.4/proj4.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/proj4leaflet/1.0.2/proj4leaflet.js"></script>
</head>
<body class="">
<div id="map" style="width: 600px; height: 400px;"></div>
</div>
<script src="main.js"></script>
</body>
I'm trying to find my way into Svelte combined with leaflet. Where I'm stuck is how to correctly split the leaflet components into files. For learning, I'm trying to build the official official leaflet quickstart with svelte.
This is how my app.svelte looks like:
<script>
import L from 'leaflet';
import { onMount } from "svelte";
import { Circle } from "./components/Circle.svelte";
let map;
onMount(async () => {
map = L.map("map");
L.tileLayer("https://a.tile.openstreetmap.org/{z}/{x}/{y}.png ", {
attribution:
'Map data © OpenStreetMap contributors, CC-BY-SA',
maxZoom: 18,
tileSize: 512,
zoomOffset: -1
}).addTo(map);
map.setView([51.505, -0.09], 13);
Circle.addTo(map);
});
</script>
<style>
html,body {
padding: 0;
margin: 0;
}
html, body, #map {
height: 100%;
width: 100vw;
}
</style>
<svelte:head>
<link
rel="stylesheet"
href="https://unpkg.com/leaflet#1.6.0/dist/leaflet.css"
integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="
crossorigin="" />
</svelte:head>
<div id="map" />
and my circle component:
<script context="module">
import L from 'leaflet';
export let map_obj;
export let Circle = L.circle([51.508, -0.11], {
color: "red",
fillColor: '#f03',
fillOpacity: 0.5,
radius: 500
});
</script>
While this is working I do not think it's effective to consider every component and add it to the map with Circle.addTo(map);. How could I pass in the map object to the circle component or is there some better pattern to build the map with several components?
Note: I do know of svelte/leaflet but like to start from scratch for learning.
This seemingly easy task is complicated due to the not-really-straightforward lifecycle of frameworks like Svelte, and the really-straightforward let-me-do-DOM-stuff architecture of Leaflet.
There are several approaches to this. I'll describe one, based on nesting Svelte components for Leaflet layers inside a Svelte component for a Leaflet map, and using setContext and getContext to handle the Leaflet L.Map instance around. (I'm borrowing this technique from https://github.com/beyonk-adventures/svelte-mapbox )
So a Svelte component for a L.Marker would look like:
<script>
import L from 'leaflet';
import { getContext } from "svelte";
export let lat = 0;
export let lng = 0;
let map = getContext('leafletMapInstance');
L.marker([lat, lng]).addTo(map);
</script>
Easy enough - get the L.Map instance from the Svelte context via getContext, instantiate the L.Marker, add it. This means that there must be a Svelte component for the map setting the context, which will need the components for the markers slotted in, i.e.
<script>
import LeafletMap from './LeafletMap.svelte'
import LeafletMarker from './LeafletMarker.svelte'
</script>
<LeafletMap>
<LeafletMarker lat=40 lng=-3></LeafletMarker>
<LeafletMarker lat=60 lng=10></LeafletMarker>
</LeafletMap>
...and then the Svelte component for the Leaflet map will create the L.Map instance, set it as the context, and be done, right? Not so fast. This is where things get weird.
Because of how Svelte lifecycle works, children components will get "rendered" before parent components, but the parent component needs a DOM element to create the L.Map instance (i.e. the map container). So this could get delayed until the onRender Svelte lifecycle callback, but that would happen after the slotted children get instantiated and their onRender lifecycle callbacks are called. So waiting for Svelte to instantiate a DOM element to contain the map and then instantiate the L.Map and then pass that instance to the context and then getting the context in the marker elements can be quite a nightmare.
So instead, an approach to this is to create a detached DOM element, instantiate a L.Map there, i.e. ...
let map = L.map(L.DomUtil.create('div')
...set it in the context, i.e. ...
import { setContext } from "svelte";
setContext('leafletMapInstance', map);
...this will allow Leaflet layers instantiated by the slotted components to be added to a detached (and thus invisible) map. And once all the lifecycle stuff lets the Svelte component for the L.Map have an actual DOM element attached to the DOM, attach the map container to it, i.e. have this in the HTML section of the Svelte component...
<div class='map' bind:this={mapContainer}>
...and once it's actually attached to the DOM, attach the map container to it and set its size, i.e. ...
let mapContainer;
onMount(function() {
mapContainer.appendChild(map.getContainer());
map.getContainer().style.width = '100%';
map.getContainer().style.height = '100%';
map.invalidateSize();
});
So the entire Svelte component for this Leaflet L.Map would look more or less like...
<script>
import L from "leaflet";
import { setContext, onMount } from "svelte";
let mapContainer;
let map = L.map(L.DomUtil.create("div"), {
center: [0, 0],
zoom: 0,
});
setContext("leafletMapInstance", map);
console.log("map", map);
L.tileLayer("https://a.tile.openstreetmap.org/{z}/{x}/{y}.png ", {
attribution:
'Map data © OpenStreetMap contributors, CC-BY-SA',
}).addTo(map);
onMount(() => {
mapContainer.appendChild(map.getContainer());
map.getContainer().style.width = "100%";
map.getContainer().style.height = "100%";
map.invalidateSize();
});
</script>
<svelte:head>
<link
rel="stylesheet"
href="https://unpkg.com/leaflet#1.6.0/dist/leaflet.css"
integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="
crossorigin=""
/>
</svelte:head>
<style>
.map {
height: 100vh;
width: 100vw;
}
</style>
<div class="map" bind:this="{mapContainer}">
<slot></slot>
</div>
See a working example here.
As a side note, I'll say that one should think it twice before sandwiching Leaflet in another JS framework, and think twice about the architecture for this (slotted components seem the cleanest and most extensible, but maybe a big data structure and some imperative programming for the Leaflet bits would be simpler). Sometimes, making sense of the lifecycle implications of more than one framework working at once can be very confusing, and very time-consuming when bugs appear.
I have a Leaflet map that I am trying to plot multiple points using map.fitbounds() but I would like the set a minZoom so the user cant zoom all the way out past a zoom level of 9. I currently use the below to add pins to a map except sometimes when a map as multiple points where the map would need to fitbounds at a wider zoom it causes the map to fail. When the map fitbounds is set higher than 9 then it works fine but below 9 it doesnt plot.
I was wondering if someone would know how to both safeguard a map from failing with a minZoom and using fitBounds in certain instances and to also stop a user from zooming so far out.
map.options.minZoom = 9;
map.options.maxZoom = 15;
var fg = L.featureGroup().addTo(map);
for ( var i=0; i < markersArray.length; ++i )
{
var linkid = (markersArray[i]['linkid']);
var linkurl = (markersArray[i]['linkurl']);
L.marker( [markersArray[i].lat, markersArray[i].lng], {icon: myIcon} )
.bindPopup( '<div id="mapPinDetails"><h3>' + markersArray[i].name + '</h3>' + markersArray[i].address + '<br /></div>' )
.addTo(fg);
}
map.fitBounds(fg.getBounds());
below 9 it doesnt plot
It does, but since you do not allow your map to zoom out below 9, it remains at zoom level 9 and you may not see your outer markers. But if you pan you can see them. The view center is the same that would have been achieved with a lower zoom:
var map = L.map('map', {
minZoom: 9,
maxZoom: 15,
}).setView([48.86, 2.35], 11);
var fg = L.featureGroup().addTo(map);
L.marker([49, 2.35]).addTo(fg);
L.marker([48.5, 2.35]).addTo(fg);
map.fitBounds(fg.getBounds());
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
<link rel="stylesheet" href="https://unpkg.com/leaflet#1.3.1/dist/leaflet.css" integrity="sha512-Rksm5RenBEKSKFjgI3a41vrjkw4EVPlJ3+OiI65vTjIdo9brlAacEuKOiQ5OFh7cOI1bkDwLqdLw3Zg0cRJAAQ==" crossorigin="" />
<script src="https://unpkg.com/leaflet#1.3.1/dist/leaflet-src.js" integrity="sha512-IkGU/uDhB9u9F8k+2OsA6XXoowIhOuQL1NTgNZHY1nkURnqEGlDZq3GsfmdJdKFe1k1zOc6YU2K7qY+hF9AodA==" crossorigin=""></script>
<div id="map" style="height: 180px"></div>
If you need further help, you will have to explain what you mean by "it causes the map to fail" and if possible share a reproducible example.
I'd like to make a simple canvas layer (not tiled canvases, but one big canvas), but I can not find how can I put layer outside mapPane to make it non-draggable in a documented way.
Should I use 'non-documented' methods or should I use 'reverse-tranform' hack ?
If I understand correctly, you would like to overlay your own canvas onto a Leaflet map, but so that it does not pan (is being dragged) with the rest of the map like the Tile Layers or Markers.
Therefore it would be like a Control (like the Zoom, Layers switching and attribution Controls) that remains at the same position relative to the map container, except that it would cover the entire map view port.
As you seem to have figured out, as soon as you insert your element into the map pane, it will move with the rest of the map elements as user drags / pans around.
Therefore you could simply append it into the map container, as a sibling of the map pane:
// http://leafletjs.com/reference-1.3.0.html#map-getcontainer
map.getContainer().appendChild(myCanvasElement);
Then you need to adjust the CSS of your canvas element:
position it absolutely
above the other siblings (the map pane has a z-index of 400, but you probably want to remain below other controls, which have a z-index of 1000)
let mouse events go through (so that user can still use map objects like clicking on Markers, etc.)
#myCanvasElement {
position: absolute;
/* Let mouse events go through to reach the map underneath */
pointer-events: none;
/* Make sure to be above the map pane (.leaflet-pane) */
z-index: 450;
}
A working code snippet example:
var map = L.map('map').setView([48.86, 2.35], 11);
var myCanvasElement = document.getElementById('myCanvasElement');
// Adjust the canvas size, assuming we want to cover the entire map.
var mapSize = map.getSize(); // http://leafletjs.com/reference-1.3.0.html#map-getsize
myCanvasElement.width = mapSize.x;
myCanvasElement.height = mapSize.y;
// Move the canvas inside the map container.
var mapContainer = map.getContainer(); // http://leafletjs.com/reference-1.3.0.html#map-getcontainer
mapContainer.appendChild(myCanvasElement);
// Draw on the canvas...
var context = myCanvasElement.getContext('2d');
context.strokeStyle = 'rgb(0, 0, 200)';
var w = 200;
var h = 100;
var x = (mapSize.x - w) / 2;
var y = (mapSize.y - h) / 2;
context.strokeRect(x, y, w, h);
L.marker([48.86, 2.35]).bindPopup('Paris').addTo(map);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
#myCanvasElement {
position: absolute;
/* Let mouse events go through to reach the map underneath */
pointer-events: none;
/* Make sure to be above the map pane (.leaflet-pane) */
z-index: 450;
}
<link rel="stylesheet" href="https://unpkg.com/leaflet#1.3.1/dist/leaflet.css" integrity="sha512-Rksm5RenBEKSKFjgI3a41vrjkw4EVPlJ3+OiI65vTjIdo9brlAacEuKOiQ5OFh7cOI1bkDwLqdLw3Zg0cRJAAQ==" crossorigin="" />
<script src="https://unpkg.com/leaflet#1.3.1/dist/leaflet-src.js" integrity="sha512-IkGU/uDhB9u9F8k+2OsA6XXoowIhOuQL1NTgNZHY1nkURnqEGlDZq3GsfmdJdKFe1k1zOc6YU2K7qY+hF9AodA==" crossorigin=""></script>
<div id="map" style="height: 180px"></div>
<canvas id="myCanvasElement"></canvas>
I am using Leaflet to create a map game (very basic).
Basically I want to add an input <div> on the map so that when a user types in a location it will pan to a coordinate on the map.
I have tried creating elements and appending to the map <div> with variations of:
var d1 = document.getElementsByClassName('leaflet-control-container')[0];
d1.insertAdjacentHTML('afterbegin', '<div id="two">two</div>');
But the <div> is displayed behind the map and the image covers it.
How can I get it to show like the Zoom Control?
If I understand correctly, you would like to create your own "Control" (somehow visually similar to the Leaflet default Zoom Control, but with different functionality), that would allow looking for different locations and navigate to them.
As for styling a Control similar to Leaflet default ones (zoom, layers control), you need to:
Extend L.Control
Specify an onAdd method that returns the DOM element to be used as Control on the map. Steps 1 and 2 will make your Control add-able to a map corner as a standard Control, with proper z-index and margin.
Style it using your own class. To get a visual effect similar to the Zoom and Layers Controls, you can build on the leaflet-bar class:
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65);
border-radius: 5px;
}
Example: (derived from the "Extending Leaflet: Handlers and Controls" tutorial)
var map = L.map('map').setView([48.86, 2.35], 11);
L.Control.MyControl = L.Control.extend({
onAdd: function(map) {
var el = L.DomUtil.create('div', 'leaflet-bar my-control');
el.innerHTML = 'My Control';
return el;
},
onRemove: function(map) {
// Nothing to do here
}
});
L.control.myControl = function(opts) {
return new L.Control.MyControl(opts);
}
L.control.myControl({
position: 'topright'
}).addTo(map);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
.my-control {
background: #fff;
padding: 5px;
}
<link rel="stylesheet" href="https://unpkg.com/leaflet#1.3.0/dist/leaflet.css" integrity="sha512-Rksm5RenBEKSKFjgI3a41vrjkw4EVPlJ3+OiI65vTjIdo9brlAacEuKOiQ5OFh7cOI1bkDwLqdLw3Zg0cRJAAQ==" crossorigin="" />
<script src="https://unpkg.com/leaflet#1.3.0/dist/leaflet-src.js" integrity="sha512-2h9aokfcaYW7k0VPn1JqbQDQCaNQRrZJwetlnQ88yJrtIzGLVW/2StdQKoE+TIVNNTUxf6SVa+2vW2KB2EXnnA==" crossorigin=""></script>
<div id="map" style="height: 200px"></div>
That being said, the Control functionality that you would like to implement sounds very similar to that of the Leaflet Control Search plugin (aka "leaflet-search")
A Leaflet control that search markers/features location by custom property.