Getting the map bounds of a GeoJson region in leaflet prior to zooming - leaflet

I'm using leaflet and I'm loading up the regions of my map dynamically from a database, based on the bounding box coordinates of the currently viewed map. As I zoom in, the detail of each new layer increases. The same regions will exist in every layer of the map, and the same region on a different zoom level will have the same id.
I am currently attempting to calculate the target map bounds and target zoom level, so that I can load up all the intersecting regions within the new map bounding box. I currently have the following code.
zoomToFeature(e) {
//e.g. map zoom for currently visible map is 3
const layer = e.target;
let padding = [5, 5];
let layerBounds = layer.getBounds();
//e.g layerBounds returns:
//ne = lat: -37.770025, lng: 145.02439
//sw = lat: -37.834451, lng: 144.900952
let targetZoom = this.map.getBoundsZoom(layerBounds, false, padding);
targetZoom = Math.min(this.map.getMaxZoom(), targetZoom);
//e.g.targetZoom for this feature is 12
let center = layer.getCenter()
//e.g. center for this layer is lat: -37.78808138412046, lng: 144.93164062500003
let targetPixelBounds = this.map.getPixelBounds(center, targetZoom);
//e.g. targetPixelBounds: max: Point{x: 946751, y: 643578} min:{x:946114,y:643078}
//this looks very wrong, and so causes everything below to fail I think.
//am I supposed to reset the origin? am I meant to project the center and targetZoom?
let sw = this.map.unproject(targetPixelBounds.getBottomLeft());
let ne = this.map.unproject(targetPixelBounds.getTopRight());
let targetMapBounds = new L.LatLngBounds(sw, ne);
this.map.flyTo(center,targetZoom);
this.loadMapData(targetMapBounds, targetZoom).subscribe(() => {
this.removeOldRegions(); // deletes existing geojson
this.loadRegions(); // adds retrieved data to new geojson layer
//find the same region but in the new zoom layer
let newLayer = this.getLayerById(layer.feature.properties.id);
this.highlightFeature(newLayer);
});
}
It is going wrong at the
let targetPixelBounds = this.map.getPixelBounds(center, targetZoom)
line.
Any idea how I can fix this?

Ok, I've cracked the case.
The unproject lines in the original code above take zoom level as an argument, which I didn't put in the original code.. So I've refactored the code, which I have posted below.
Basically once I have the map bounding box, I can perform the fly to, and execute code to retrieve the new geojson at the new zoom level. By the time the animation has completed, the new geojson layer is already loaded. (Written in TypeScript. Sorry non TypeScript people)
zoomToFeature(e) {
const layer = e.target;
let padding = [5, 5];
let layerBounds = layer.getBounds();
let targetMapBoundsZoom = this.getTargetMapBoundsZoom(layerBounds, { padding: padding });
this.map.flyToBounds(targetMapBoundsZoom.bounds);
this.loadMapData(targetMapBoundsZoom.bounds, targetMapBoundsZoom.zoom).subscribe(() => {
this.removeOldRegions();
this.loadRegions();
//find the same region but in the new zoom layer
let newLayer = this.getLayerById(layer.feature.properties.id);
this.highlightFeature(newLayer);
});
}
getTargetMapBoundsZoom(bounds, options) {
let newBoundsCenterZoom = this._getBoundsCenterZoom(bounds, options);
let targetMapBoundsPixels = this.map.getPixelBounds(newBoundsCenterZoom.center, newBoundsCenterZoom.zoom);
let targetSw = this.map.unproject(targetMapBoundsPixels.getBottomLeft(), newBoundsCenterZoom.zoom);
let targetNe = this.map.unproject(targetMapBoundsPixels.getTopRight(), newBoundsCenterZoom.zoom);
let targetMapBounds = new L.LatLngBounds(targetSw, targetNe);
return {
bounds: targetMapBounds,
zoom: newBoundsCenterZoom.zoom
}
}
loadMapData(bounds, zoom): Observable<any[]> {
const boundingBox = Util.GetMapBounds(bounds);
const regionTypeId = this.regionTypeIds[this._regionType];
return this.mapService.getGeoJsonData("AU",
regionTypeId,
zoom,
boundingBox.n,
boundingBox.s,
boundingBox.e,
boundingBox.w).pipe(map((response: any) => {
this.mapGeoJsonData = this.createFeatureCollection(response.data);
return this.mapGeoJsonData;
}));
}
//this is a copy of the original _getBoundsCenterZoom that is internal to leaflet, with minor modifications.
_getBoundsCenterZoom(bounds, options) {
options = options || {};
bounds = bounds.getBounds ? bounds.getBounds() : L.latLngBounds(bounds);
var paddingTL = L.point(options.paddingTopLeft || options.padding || [0, 0]),
paddingBR = L.point(options.paddingBottomRight || options.padding || [0, 0]),
zoom = this.map.getBoundsZoom(bounds, false, paddingTL.add(paddingBR));
zoom = (typeof options.maxZoom === 'number') ? Math.min(options.maxZoom, zoom) : zoom;
if (zoom === Infinity) {
return {
center: bounds.getCenter(),
zoom: zoom
};
}
var paddingOffset = paddingBR.subtract(paddingTL).divideBy(2),
swPoint = this.map.project(bounds.getSouthWest(), zoom),
nePoint = this.map.project(bounds.getNorthEast(), zoom),
center = this.map.unproject(swPoint.add(nePoint).divideBy(2).add(paddingOffset), zoom);
return {
center: center,
zoom: zoom
};
}
createFeatureCollection(data: any) {
let featureCollection = {
type: "FeatureCollection",
features: data.map(r => {
const geoJson = JSON.parse(r.geoJson);
if (geoJson.type === "GeometryCollection") {
geoJson.geometries = geoJson.geometries.filter(r => r.type === "Polygon" || r.type === "MultiPolygon");
}
let feature = {
type: "Feature",
id: r.id,
properties: { id: r.id },
geometry: geoJson
}
return feature;
})
};
return featureCollection;
}

The default behaviour for this is to read out the bounds and pass it to the map:
map.fitBounds(e.target.getBounds());
Then you function can look like:
zoomToFeature(e) {
let padding = [5, 5];
let layerBounds = e.target.getBounds();
let targetZoom = this.map.getBoundsZoom(layerBounds, false, padding);
targetZoom = Math.min(this.map.getMaxZoom(), targetZoom);
//e.g.targetZoom for this feature is 12
map.fitBounds(layerBounds , {padding: padding, maxZoom: targetZoom });
this.loadMapData(layerBounds , targetZoom).subscribe(() => {
this.removeOldRegions(); // deletes existing geojson
this.loadRegions(); // adds retrieved data to new geojson layer
//find the same region but in the new zoom layer
let newLayer = this.getLayerById(layer.feature.properties.id);
this.highlightFeature(newLayer);
});
}
this moves the map and zoom it to the layer.

Related

MapQuest/Leaflet - How to trace routes between marquers with gps coordinates?

I have managed to integrate mapquest within my leaflet maps which was initially showing markers on the map. Below is an example with markers showing photographs taking during a trip in Namibia.
https://www.paulgodard.com/map?c=2108_DesolationValley&p=travel&m=images
Terms
Blog
Routes between marquers with gps coordinates
1 post / 0 new
Quick reply
Thu, 08/19/2021 - 06:10
#1
Paul Godard
Routes between marquers with gps coordinates
I have managed to integrate mapquest within my leaflet maps which was initially showing markers on the map. Below is an example with markers showing photographs taking during a trip in Namibia.
https://www.paulgodard.com/map?c=2108_DesolationValley&p=travel&m=images
I already have an array of locations and I would like to display the routes in between each marker. What is the best way to do this?
window.mapData = #json($mapData);
window.onload = function() {
L.mapquest.key = 'mykey';
var map = L.mapquest.map('mapOSM', {
center: [0,0],
layers: L.mapquest.tileLayer('map'),
zoom: 10
});
map.addControl(L.mapquest.control());
var mainIcon = L.Icon.extend({ options: {
iconSize: [24,24],
iconAnchor: [12,24], // half of x | full y
popupAnchor: [0,-12] // x = 0 | - half y
}});
var oms = new OverlappingMarkerSpiderfier(map);
var bounds = new L.LatLngBounds();
for (var i = 0; i < window.mapData.length; i ++) {
var datum = window.mapData[i];
var loc = new L.LatLng(datum.lat, datum.lon);
bounds.extend(loc);
var mapIconURL = '/public/assets/icons/' + datum.icon;
mapIconURL = mapIconURL.replace(/\s+/g,'');
var marker = new L.Marker(loc, { icon: new mainIcon({iconUrl: mapIconURL}) });
marker.desc = datum.popup; //JSON.parse(datum.popup);
//if ($i=0) { alert(datum.popup); }
map.addLayer(marker);
oms.addMarker(marker);
}
if (window.mapData.length > 0) {
map.fitBounds(bounds);
} else {
map.center(window.mapData[0].lat,window.mapData[0].lon);
map.zoom(1);
}
var popup = new L.Popup({closeButton: false, offset: new L.Point(0.5, -24)});
oms.addListener('click', function(marker) {
popup.setContent(marker.desc);
popup.setLatLng(marker.getLatLng());
map.openPopup(popup);
});
oms.addListener('spiderfy', function(markers) { map.closePopup(); });
oms.addListener('unspiderfy', function(markers) { });
}
You can start with the Leaflet Routing Plugin here: https://developer.mapquest.com/documentation/leaflet-plugins/routing/
Routing in Namibia might get iffy though.

Make boundary for ol static image

I'm using ol v5.3.0 for view static image.
I want to bound the image for the size of the ol div.
When the image minimize more than the div size the will not minimize anymore.
I tried to use the projection attribute of the static image with the extent of the original image size
But it does not help.
there is any option to do it?
Based on this example https://openlayers.org/en/v4.6.5/examples/static-image.html
Instead of opening immediately at zoom 2, if first you fit the extent to the map size and get the zoom level (which will be fractional) which that produces and set it as the minimum zoom the map cannot be zoomed out beyond the size of the image (although it is possible to pan outside the extent).
var extent = [0, 0, 1024, 968];
var projection = new ol.proj.Projection({
code: 'xkcd-image',
units: 'pixels',
extent: extent
});
var map = new ol.Map({
layers: [
new ol.layer.Image({
source: new ol.source.ImageStatic({
attributions: '© xkcd',
url: 'https://imgs.xkcd.com/comics/online_communities.png',
projection: projection,
imageExtent: extent
})
})
],
target: 'map',
view: new ol.View({
projection: projection
})
});
var view = map.getView();
view.fit(extent, { constrainResolution: false });
view.setMinZoom(view.getZoom());
// set opening zoom level (zoom set by code must not be less than the minZoom)
view.setZoom(Math.max(2, view.getMinZoom());
var center = ol.extent.getCenter(extent);
view.on(['change:center','change:resolution'], function() {
// map.on('moveend', function() { // alternative
var viewCenter = view.getCenter();
if ((viewCenter[0] != center[0] || viewCenter[1] != center[1]) && view.getZoom() == view.getMinZoom()) {
view.setCenter(center);
}
});

Changing overlay layers when switching base layer

I have built a leaflet map with two base layers, and each of these base layers will have their own unique points of interest. The points of interest are being stored as geojson that I loop over to create multiple overlays for different categories. So when viewing the default base layer you would see layers for Show All, Cat1, Cat2 etc.
I need a way to be able to attach overlay layers to a base layer, or remove all overlay layers and then load the relevant ones when the base layer changes.
I tried using the following, which worked to switch categories, with the baselayerchange event, but the overlay layers were still displaying when I switched base layers.
layerControl._layers.forEach(function(layer){
if(layer.overlay){
map.removeLayer(layer.layer)
}
});
I've been searching for a couple of days now for an answer to this without any luck, any help is greatly appreciated.
EDIT
Posting additional code for context. This is not the entirety of the code, there are some plugins that I'm not including code for and have excluded definitions for a several variables, but this should provide better insight into how things are working.
//Initialize the map
var map = new L.Map('map', {
maxZoom: mapMaxZoom,
minZoom: mapMinZoom,
crs: crs1848,
attributionControl: false,
layers: [pano1848]
});
//add controls to the map
var layerControl = L.control.layers(null, null, {position: 'bottomleft'}).addTo(map);
//building category layers from geojson
var types = ['African Americans', 'Art Architecture Culture', 'Education Religion Reform', 'Everyday Life', 'Immigrants', 'Science Inventions', 'Transportation Industry Commerce'];
types.forEach(function(type){
var catType = type.replace(/\s/g,"");
var catPoints = L.geoJson(mapData, {
filter: function(feature, layer){
var cat = feature.properties['category'];
return cat.indexOf(catType) >= 0;
},
onEachFeature: function (feature, layer) {
layer.bindTooltip(feature.properties.name);
(function(layer, properties){
//Create Numeric markers
var numericMarker = L.ExtraMarkers.icon({
icon: 'fa-number',
markerColor: 'yellow',
number: feature.properties['id']
});
layer.setIcon(numericMarker);
layer.on('click', function() {
$.ajax({
url:feature.properties['url'],
dataType:'html',
success: function(result){
$('#detailContainer').html(result);
$('#overlay').fadeIn(300);
}
});
});
})(layer, feature.properties);
}
});
layerControl.addOverlay(catPoints, catType);
});
//Base Layer Change Event
map.on('baselayerchange', function(base){
var layerName;
layerControl._layers.forEach(function(layer){
if(layer.overlay){
map.removeLayer(layer.layer)
}
});
if(base._url.indexOf('1848') >= 0){
map.options.crs = crs1848;
map.fitBounds([
crs1848.unproject(L.point(mapExtent1848[2], mapExtent1848[3])),
crs1848.unproject(L.point(mapExtent1848[0], mapExtent1848[1]))
]);
var southWest = map.unproject([0, 8192], map.getMaxZoom());
var northEast = map.unproject([90112, 0], map.getMaxZoom());
map.setMaxBounds(new L.LatLngBounds(southWest, northEast));
map.addLayer(allPoints);
layerName = '1848 Panorama';
}
else if(base._url.indexOf('2018') >= 0){
map.options.crs = crs2018;
map.fitBounds([
crs2018.unproject(L.point(mapExtent2018[2], mapExtent2018[3])),
crs2018.unproject(L.point(mapExtent2018[0], mapExtent2018[1]))
]);
var southWest = map.unproject([0, 8192], map.getMaxZoom());
var northEast = map.unproject([49152, 0], map.getMaxZoom());
map.setMaxBounds(new L.LatLngBounds(southWest, northEast));
layerName = '2018 Panorama'
}
miniMap.changeLayer(minimapLayers[layerName]);
//map.setView(map.getCenter(), map.getZoom());
});
You may create global variable call "overlays", and remove it like an example below.
Here is the similar example to illustrate your problem jsFiddle
var overlays = {
'Name 1': catPoints,
'Name 2': catType
};
L.control.layers(null, overlays).addTo(map);
// Whenever you want to remove all overlays:
for (var name in overlays) {
map.removeLayer(overlays[name]);
}

Leaflet: impossible to remove specific image layer

Below are three functions that I use to respectively create a map, add an IMG-layer and remove an IMG-layer. Problem is that when I add an IMG-layer and then want to remove it before adding a new (different) IMG-layer......the removal is not taking place.
The IMG-layers that I use are simple URLs to png's.
So subsequently:
loadMap();
SetLayer("2018_10_13_16_15.png");
RemoveLayer("2018_10_13_16_15.png");
SetLayer("2018_10_13_16_30.png");
Then "2018_10_13_16_15.png" is not removed.
What am I doing wrong??
var map = L.map('map');
var imageBounds = [[50.520, 0.813], [54.295, 10.996]];
function loadMap(){
map.createPane('labels');
var positron = L.tileLayer('http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', {
attribution: cartodbAttribution
}).addTo(map);
var positronLabels = L.tileLayer('http://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}.png', {
attribution: cartodbAttribution,
pane: 'labels'
}).addTo(map);
map.setView({lat: 52.5, lng: 5.2}, 6.5);
isMap = true; //map is loaded
}
function SetLayer(url) {
IMGLayer = L.imageOverlay(url, imageBounds);
IMGLayer.addTo(map);
}
function RemoveLayer(url) {
IMGLayer = L.imageOverlay(url, imageBounds);
IMGLayer.removeFrom(map);
}

How do I get the bounding box of a mapboxgl.GeoJSONSource object?

I'm setting up a Mapbox GL JS map like this:
mapboxgl.accessToken = 'pk.my_token';
var cityBoundaries = new mapboxgl.GeoJSONSource({ data: 'http://domain.com/city_name.geojson' } );
var map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v8',
center: [cityLongitude,cityLatitude],
zoom: 13
});
Then I'm loading that GeoJSON data onto the map after it loads like this:
map.on('style.load', function(){
map.addSource('city', cityBoundaries);
map.addLayer({
'id': 'city',
'type': 'line',
'source': 'city',
'paint': {
'line-color': 'blue',
'line-width': 3
}
});
});
At this point, I have a map that's centered at the location I specified in new mapboxgl.Map, and it's at zoom level 13. So, only a piece of the GeoJSON data is visible on the map. I'd like to re-center and re-zoom the map so that the entire GeoJSON data is visible.
In Mapbox JS, I would do this by loading the GeoJSON data into a featureLayer and then fitting the map to its bounds with:
map.fitBounds(featureLayer.getBounds());
The fitBounds documentation for Mapbox GL JS indicates that it wants the bounds in the format of [[minLng, minLat], [maxLng, maxLat]].
Is there a way to determine the mix/max latitude & longitude values of this GeoJSON layer?
Based on the 'Obtaining a bounding box' section of this post, I've come up with this process...
map.on('style.load', function(){
$.getJSON('http://citystrides.dev/city_name.geojson', function(response){
var boundingBox = getBoundingBox(response);
var cityBoundary = new mapboxgl.GeoJSONSource({ data: response } );
map.addSource('city', cityBoundary);
map.addLayer({
'id': 'city',
'type': 'line',
'source': 'city',
'paint': {
'line-color': 'blue',
'line-width': 3
}
});
map.fitBounds([[boundingBox.xMin, boundingBox.yMin], [boundingBox.xMax, boundingBox.yMax]]);
})
});
function getBoundingBox(data) {
var bounds = {}, coords, point, latitude, longitude;
for (var i = 0; i < data.features.length; i++) {
coords = data.features[i].geometry.coordinates;
for (var j = 0; j < coords.length; j++) {
longitude = coords[j][0];
latitude = coords[j][1];
bounds.xMin = bounds.xMin < longitude ? bounds.xMin : longitude;
bounds.xMax = bounds.xMax > longitude ? bounds.xMax : longitude;
bounds.yMin = bounds.yMin < latitude ? bounds.yMin : latitude;
bounds.yMax = bounds.yMax > latitude ? bounds.yMax : latitude;
}
}
return bounds;
}
Here's a walkthrough of what the code is doing, for anyone out there who needs a detailed explanation:
map.on('style.load', function(){
When the map loads, let's do the stuff in this function.
$.getJSON('http://citystrides.dev/city_name.geojson', function(response){
Get the city's GeoJSON data. This is an asynchronous call, so we have to put the all the code that uses this data (the response) inside this function.
var boundingBox = getBoundingBox(response);
Get the bounding box of this GeoJSON data. This is calling the , function(){ that appears after the 'map on style load' block.
var cityBoundary = new mapboxgl.GeoJSONSource({ data: response } );
Build Mapbox's GeoJSON data.
map.addSource('city', cityBoundary);
Add the source to Mapbox.
map.addLayer({
Add the layer to Mapbox.
map.fitBounds([[boundingBox.xMin, boundingBox.yMin], [boundingBox.xMax, boundingBox.yMax]]);
Adjust the map to fix the GeoJSON data into view.
function getBoundingBox(data) {
This function iterates over the returned GeoJSON data, finding the minimum and maximum latitude and longitude values.
One thing to note in the getBoundingBox function is this line:
coords = data.features[i].geometry.coordinates;
In the original post, linked above, this line was written as coords = data.features[i].geometry.coordinates[0]; because their data for the list of coordinates was an array of arrays. My data isn't formatted that way, so I had to drop the [0]. If you try this code & it blows up, that might be the reason.
You can use the turf.js library. It has a bbox function:
const bbox = turf.bbox(foo);
https://turfjs.org/docs/#bbox
I use the turf-extent library, which is maintained by the Mapbox bunch anyhow. https://www.npmjs.com/package/turf-extent is the node module link.
In your code you simply import(ES6) or require as so:
ES6/Webpack: import extent from 'turf-extent';
Via script tag: `<script src='https://api.mapbox.com/mapbox.js/plugins/turf/v2.0.2/turf.min.js'></script>`
Then feed your response to the function, for example:
ES6/Webpack: let orgBbox = extent(response);
Normal: var orgBbox = turf.extent(geojson);
Then you can use the array values to set your map center:
center: [orgBbox[0], orgBbox[1]]
Or as you want, to fit bounds:
map.fitBounds(orgBbox, {padding: 20});
Here is an example using the turf.min.js in a regular html tag in case you are not using webpack or browser:
https://bl.ocks.org/danswick/83a8ddff7fb9193176a975a02a896792
Happy coding and mapping!
Based on James Chevalier's answer. For polygon/multipolygon tilesets that are assigend to a map in Mapbox Studio I am using this to get the bounding box:
getPolygonBoundingBox: function(feature) {
// bounds [xMin, yMin][xMax, yMax]
var bounds = [[], []];
var polygon;
var latitude;
var longitude;
for (var i = 0; i < feature.geometry.coordinates.length; i++) {
if (feature.geometry.coordinates.length === 1) {
// Polygon coordinates[0][nodes]
polygon = feature.geometry.coordinates[0];
} else {
// Polygon coordinates[poly][0][nodes]
polygon = feature.geometry.coordinates[i][0];
}
for (var j = 0; j < polygon.length; j++) {
longitude = polygon[j][0];
latitude = polygon[j][1];
bounds[0][0] = bounds[0][0] < longitude ? bounds[0][0] : longitude;
bounds[1][0] = bounds[1][0] > longitude ? bounds[1][0] : longitude;
bounds[0][1] = bounds[0][1] < latitude ? bounds[0][1] : latitude;
bounds[1][1] = bounds[1][1] > latitude ? bounds[1][1] : latitude;
}
}
return bounds;
}