I have a map which is refreshed every 30 seconds and on each refresh I fit bounds to the new data. I would like to change the way I fit bounds if a user has manipulated the map, e.g. changed zoom level. My problem is that zoom triggered from fitBounds is indistinguishable from user action.
How do I capture/extend mousewheel on the map?
Basically there are multiple ways todo same, but you can go with following simple way to distinguish user zoom triggered.
The onwheel event occurs when the mouse wheel is rolled up or down over an element.
var map = L.map('map', {
// Set latitude and longitude of the map center (required)
center: [12.99766, -84.90838],
// Set the initial zoom level, values 0-18, where 0 is most zoomed-out (required)
zoom: 5,
// scrollWheelZoom: false
});
// Create a Tile Layer and add it to the map
var tiles = new L.tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png').addTo(map);
document.getElementById("map").addEventListener("wheel", myFunction);
function myFunction() {
alert("Mouse wheel Scrolled by user.")
}
#map {
width: 500px;
height: 400px;
}
<link rel="stylesheet" href="https://unpkg.com/leaflet#1.0.3/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet#1.0.3/dist/leaflet.js"></script>
<div id="map"></div>
Hope this will helps you.
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'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.
I have an instance of a Mapbox GL map, after load of my data source I am calling fitBounds() to change the map's center and zoom to fit my data set. I've also attached a number of event listeners to this map because I want to know when the user manually changed the map's zoom or position.
Mapbox also triggers 'movestart' and 'zoomstart' on fitBounds(), though I'm getting around that problem by checking for the presence of the originalEvent property in the event callback.
The problem is, I also have a NavigationControl added to the map, and user interactions triggered through its zoom or rotate buttons fire my map events without the originalEvent property. I cannot find any way in the Mapbox documentation to listen attach event listeners to the NavigationControl, nor a way to differentiate between a zoom / pan initiated by a fitBounds call vs. a user interaction through that component.
Is there something I'm missing? Is there a way to attach mouse / touch event listeners to the NavigationControl component? Or perhaps is there some property within the event objects that will tell me the source of the event?
Simplified code sample -
this._userMoved = false;
this._map = new mapboxgl.Map(options);
// listen for user actions that update the map display
['movestart', 'zoomstart', 'boxzoomstart', 'rotatestart', 'pitchstart'].forEach((action) => {
this._map.on(action, (e) => {
if (e.originalEvent) {
// if this property is set, the event in question was triggered by an actual user ineraction.
// EXCEPT when the user interaction came from the NavigationControl, hence the problem
this._userMoved = true;
}
});
});
this._map.on('load', () => {
// add the control after map load
this._map.addControl(new mapboxgl.NavigationControl(),'top-left');
this._setMapData(); // adds my data source to the map
this._setMapView(); // calls this._map.fitBounds() using my data source
});
If your need is specifically to handle a specific event (fitbounds) that is being called once, then you can do this:
this._map.once('moveend', e => {
// do whatever you do after the fitbounds event.
this._map.on(['movestart', 'zoomstart', 'boxzoomstart', 'rotatestart', 'pitchstart'], userMovementHandler)
});
EDIT
I just looked more closely at the documentation and there is indeed an eventData parameter to fitBounds which is intended to solve exactly this problem.
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<title>Display a map</title>
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.43.0/mapbox-gl.js'></script>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.43.0/mapbox-gl.css' rel='stylesheet' />
<style>
body { margin:0; padding:0; }
#map { position:absolute; top:0; bottom:0; width:100%; }
</style>
</head>
<body>
<div id='map'></div>
<script>
mapboxgl.accessToken = 'pk.eyJ1Ijoic3RldmFnZSIsImEiOiJGcW03aExzIn0.QUkUmTGIO3gGt83HiRIjQw';
var map = new mapboxgl.Map({
container: 'map', // container id
style: 'mapbox://styles/mapbox/streets-v9', // stylesheet location
center: [-74.50, 40], // starting position [lng, lat]
zoom: 9 // starting zoom
}).on('moveend', e => {
if (e.source === 'fitBounds') {
console.log('Caused by fitBounds');
} else {
console.log('Caused by user');
}
})
map.fitBounds([140,-42, 150,-37], {}, {source: 'fitBounds'})
</script>
</body>
</html>
Is there a way to fit the map exactly to bounds? (or as close a possible).
For example, in this jsfiddle, even without padding the map leaves a lot of padding above and below the points:
http://jsfiddle.net/BC7q4/444/
map.fitBounds(bounds);
$('#fit1').click(function(){ map.fitBounds(bounds); });
$('#fit2').click(function(){ map.fitBounds(bounds, {padding: [50, 50]}); });
I'm trying to fit a map as precisely as possible to a set of bounds that closely match the map shape without all the extra padding.
Setting a map's ne corner and sw corner would work as well, but I don't think that functionality exists.
You are very probably looking for the map zoomSnap option:
Forces the map's zoom level to always be a multiple of this, particularly right after a fitBounds() or a pinch-zoom. By default, the zoom level snaps to the nearest integer; lower values (e.g. 0.5 or 0.1) allow for greater granularity. A value of 0 means the zoom level will not be snapped after fitBounds or a pinch-zoom.
Because its default value is 1, after your fitBounds the map will floor down the zoom value to the closest available integer value, hence possibly introducing more padding around your bounds.
By specifiying zoomSnap: 0, you ask the map not to snap the zoom value:
var map = L.map('map', {
zoomSnap: 0 // http://leafletjs.com/reference.html#map-zoomsnap
}).setView([51.505, -0.09], 5);
// add an OpenStreetMap tile layer
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
var southWest = new L.LatLng(40.712, -74.227),
northEast = new L.LatLng(40.774, -74.125),
bounds = new L.LatLngBounds(southWest, northEast);
L.marker([40.712, -74.227]).addTo(map);
L.marker([40.774, -74.125]).addTo(map);
map.fitBounds(bounds);
$('#fit1').click(function() {
map.fitBounds(bounds);
});
$('#fit2').click(function() {
map.fitBounds(bounds, {
padding: [50, 50]
});
});
body {
padding: 0;
margin: 0;
}
#map {
height: 300px;
margin-top: 10px;
}
<link rel="stylesheet" href="https://unpkg.com/leaflet#1.2.0/dist/leaflet.css">
<script src="https://unpkg.com/leaflet#1.2.0/dist/leaflet-src.js"></script>
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<button id="fit1">fit without bounds</button>
<button id="fit2">fit with bounds</button>
<div id="map"></div>