Leaflet.js time slider/animation with Mapbox TileLayers - leaflet

I've got a RESTful API pushing .png-based TileLayers and associated metadata (datetime, lat/lon, etc.)
I'd like to create a time slider or animation to display them as per the user's inputs.
What is a good starting point? I'm seeing some libraries which do not seem to have support for TileLayers/raster overlays. Can anyone recommend a better choice?

After perusing the literature, this is the solution I chose to start from:
http://fiddle.jshell.net/nathansnider/260hffor/
We basically build an array of tile layer strings and then use a slider to load them dynamically.
.ajax({
type: "GET",
url: {{GET ROUTE GOES HERE}},
success: function (data) {
imageLayers = [];
$.each(data, function (k, v) {
imageLayers.push(L.tileLayer(mbUrl, { id: {{TILE LAYER ID GOES HERE}}, token: {{TOKEN GOES HERE}}, format: 'png', time: {{IMAGE
DATE TIME GOES HERE}}.substr(0, 10)}))
});
map.setView([data[0].{{LATITUDE PROPERTY}}, data[0].{{LONGITUDE PROPERTY}}], 14);
layerGroup = L.layerGroup(imageLayers);
var sliderControl = L.control.sliderControl({
layer: layerGroup,
follow: null
});
map.addControl(sliderControl);
sliderControl.startSlider();
$('#slider-timestamp').html(options.markers[ui.value].feature.properties.time.substr(0, 10));

Related

Esri-Leaflet - Search within a distance

I need to design an application using a feature layer stored in ArcGIS online. Using a geocoder/search, I need to be able to enter an address and select a distance (1 block, 2 blocks, etc). The result will show the new point, a distance radius, and all points within the radius. I would also like to have a table of the results.
What I need is exactly like this app created by Derek Eder from DataMade: https://carto-template.netlify.app/, except mine needs the data stored in a secured ArcGIS layer. Can anyone point me to an example, tutorial, etc with an esri-leaflet implementation similar to this application? I have spent the past five days trying to convert the code, and I feel like I am getting no where.
Here is a link to guthub: https://github.com/datamade/searchable-map-template-carto
-------UPDATE-------
Seth - I can get the layer to display; however, the query to join the searched point with the layer does not work. I imagine I’m leaving something out, because the console error reads “token required”. See below:
const radius = 1610;
/**************************************************************************************************/
// ArcGIS Authoization
/**************************************************************************************************/
$("#loginModal").modal({ backdrop: 'static', keyboard: false });
// submit element of form
var submitBtn = document.getElementById('btnArcGISOnline');
// add event listener to form
submitBtn.addEventListener('click', addServicesFromServer);
// create map and set zoom level and center coordinates
var map = L.map('mapCanvas', {
}).setView([30.46258, -91.13171], 12);
// set basemap to Esri Streets
L.esri.basemapLayer('Streets').addTo(map);
var layerurl = 'secure/layer/URL';
var tokenUrl = 'https://www.arcgis.com/sharing/generateToken';
// function to make request to server
function serverAuth(server, username, password, callback) {
L.esri.post(server, {
username: username,
password: password,
f: 'json',
expiration: 86400,
client: 'referer',
referer: window.location.origin
}, callback);
}
// function to run when form submitted
function addServicesFromServer(e) {
// prevent page from refreshing
e.preventDefault();
// get values from form
var username = document.getElementById('username').value;
var password = document.getElementById('password').value;
// generate token from server and add service from callback function
serverAuth(tokenUrl, username, password, function (error, response) {
if (error) {
return;
}
// add layer to map
var featureLayer = L.esri.featureLayer({
url: layerurl,
opacity: 1,
token: response.token
});
featureLayer.addTo(map);
$("#loginModal").modal("hide");
}); // end serverAuth call
} // end addServicesFromServer call
// HARNESS GEOCODER RESULTS
let circle;
// GeoSearch
const search = L.esri.Geocoding.geosearch({
useMapBounds: false,
expanded: true,
collapseAfterResult: false
});
search.addTo(map);
search.on("results", (results) => {
if (results && results.latlng) {
if (circle) {
circle.remove();
}
circle = L.circle(results.latlng, { radius });
circle.addTo(map);
queryLayer(results.latlng);
}
});
// SET UP QUERY FUNCTION
function queryLayer(point) {
const query = L.esri.query({ url: layerurl }).nearby(point, radius);
query.run(function (error, featureCollection, response) {
if (error) {
console.log(error);
return;
}
console.log(featureCollection.features);
populateList(featureCollection.features);
});
}
// WRITE RESULTS INTO A LIST
function populateList(features) {
const list = document.getElementById("results-list");
let listItems = "";
features.forEach((feature) => {
listItems =
listItems +
`
<li>
Place: ${feature.properties?.Location} <br>
Lat: ${feature.properties?.Latitude} <br>
Lng: ${feature.properties?.Longitude} <br>
</li>
`;
list.innerHTML = listItems;
});
}
I attempted to pass the token to the query as pasted below, but then I get an invalid token error.
var layerUrl_token = layerurl + "?token=" + response.token;
I also tried using turf.js, but I haven’t been successful. I know turf.js uses long/lat, but I haven’t even been able to get the correct syntax to pull the lat and long from the feature layer.
What you're trying to do is not too hard. While there are a handful of tutorials on different parts of what you want to do, let's piece things together. I'm going to use esri-leaflet-geocoder for my search functionality, as its consistent with esri-leaflet, and IMO its one of the best geocoders available for leaflet.
Setting up the geocoder
After setting up a basic leaflet map, let's import esri-leaflet and esri-leaflet-geocoder, and create a geocoder:
import L from "leaflet";
import * as EL from "esri-leaflet";
import * as ELG from "esri-leaflet-geocoder";
const search = ELG.geosearch({
useMapBounds: false,
expanded: true,
collapseAfterResult: false
});
search.addTo(map);
Don't forget to add the geocoder css to your html, as shown in the documentation example.
Add your layer:
const layerurl = "YOUR_LAYER_URL";
const featureLayer = EL.featureLayer({ url: layerurl });
featureLayer.addTo(map);
If you are using an authenication-required layer, you will need to get a token and use it as one of the options in featurelayer, (featureLayer({ url: layerurl, token: token })). If you're not sure how to get a token, make a comment and I can add some code for that, but there are some nice tutorials already available for that.
Harness the results of the search
The ELG.geosearch comes with a results event that you can harness. It is called when the user selects one of the results in the autocomplete dropdown of the geosearch. In that event, we can get the location data of location the user selected. We center the map there (which is a default of the geosearch actually), draw a circle with a given radius, and perform a query (more on that layer):
let circle;
search.on("results", (results) => {
if (results && results.latlng) {
if (circle) {
circle.remove();
}
circle = L.circle(results.latlng, { radius });
circle.addTo(map);
queryLayer(results.latlng);
}
});
Query the layer
Now we know the latlng of the location the user selected from the search. We can create an esri-leaflet query, which can query your feature layer in various ways. We'll see up a nearby query, which will query the layer for any features within a given radius of a point:
function queryLayer(point) {
const query = EL.query({ url: layerurl }).nearby(point, radius);
query.run(function (error, featureCollection, response) {
if (error) {
console.log(error);
return;
}
populateList(featureCollection.features);
});
}
If you are querying an authenticated layer, you'll need to add a token to the request. I'm fairly certain the way to do this is like so:
function queryLayer(point) {
const query = EL.query({ url: layerurl })
.token(<your_token_here>)
.nearby(point, radius);
// ... same as above
}
You may also be able to run a query directly off of your layer:
featureLayer.query().nearby(point, radius)
I'm not as familiar with this second way, but you can read more about it here: Query a feature layer.
Render to the page
Once we .run the query, we will have access to the results in the form of a featureCollection. We can loop through the features of that featureCollection and render some HTML:
function populateList(features) {
const list = document.getElementById("results-list");
let listItems = "";
features.forEach((feature) => {
listItems =
listItems +
`
<li>
Place: ${feature.properties?.Location} <br>
Lat: ${feature.properties?.Latitude} <br>
Lng: ${feature.properties?.Longitude} <br>
</li>
`;
list.innerHTML = listItems;
});
}
In this particular example, I am using a point layer I made that is being served through arcgis online. This point layer does not have address data, so feature.properties doesn't contain any address info. For your featurelayer, the attributes of your layer will be available in a feature.properties. So depending on what's there, you might want to use feature.properties?.address or whatever. This last bit is just an example, you will probably customize that a lot differently for your own purposes.
Working Codesandbox
Try searching heavily populated areas in this example. Note that in this featurelayer there are many overlapping locations, so there are more results in the list than it looks like there are markers on the map.
Also note, this example I'm posting using esri-leaflet and esri-leaflet-geocoder versions 2^. These were just updated to versions 3 about 1-2 months ago, and the new versions require use of an API key in the geocoder and in the layer declaration, so if you want to use the latest versions (recommended), you will need to add those in. I used version 2 so as not to expose an API key in a sandbox (and I sort of hate the new API key requirement . The new arcgis developers documentation for esri-leaflet has some examples of that, but the official documentation has not yet been updated to match those examples.

How can I zoomed to the cql_filter feature?

I am using geoserver. I write the following code;
const mywms = L.tileLayer.wms("http://localhost:8080/geoserver/taj/wms", {
layers: 'taj:country',
format: 'image/png',
CQL_FILTER: 'name=pana'
transparent: true,
opacity: 0.4,
version: '1.1.0',
attribution: "country layer"
});
All is good. The layer get filtered. But I need the selected feature to zoom full extend.
I tried to center the mywms layer using this code; map.fitBounds(mywms.GetBounds());. But it shows the error; mywms.getBOunds is not a function. Any help?
As is pointed out in the comments WMS requests contain the bounding box of the map in the request and so will always cover the whole of the map area.
To get the extent of a single feature you need to make a WFS request with the filter included and then zoom to the extent of that feature when it is returned.
Thank you very much for all of the helping hands. And special thanks to Mr. Ina Turton who helps me to find out the actual problem and get the solution. my url for the wfs server looks like this;
http://localhost:8080/geoserver/tajikistan/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=taj%3Acountry&maxFeatures=50&outputFormat=application%2Fjson
I write the following code to for zooming into my cql_filter area:
//Geoserver Web Feature Service
$.ajax('http://localhost:8080/geoserver/wfs',{
type: 'GET',
data: {
service: 'WFS',
version: '1.1.0',
request: 'GetFeature',
typename: 'tajikistan:country',
CQL_FILTER: "name_rg='Centre'",
srsname: 'EPSG:4326',
outputFormat: 'text/javascript',
},
dataType: 'jsonp',
jsonpCallback:'callback:handleJson',
jsonp:'format_options'
});
function handleJson(data) {
var requiredArea = L.geoJson(data).addTo(map);
map.fitBounds(requiredArea.getBounds())
}
In this code the CQL_FILTER is used for to filter out the required area. In my case the data having column name name_rg which have the property as Centre. I query this region using this code. I fetch the data using ajax method. I write the code little tricky because the regular ajax call doesn't callback the handleJson function and return an parseJson error. And finally I added the variable named as requiredArea. Get bounds of this region and set bounds of the required region and added into the map.

MapboxGL: querying rendered features after multiple geocodes

Situation: I have a working site where upon entering an address, MapboxGL marks a point on the map and queries a polygon layer (queryRenderedFeatures) and displays the polygon feature containing the point.
This works; however, if I then want to geocode a second address that changes the map view, it fails the second time because map.queryRenderedFeatures returns an empty array.
var userDistrictsGeoJson;
map.on('load', function() {
//add layers from Mapbox account
addLayers(); //details left out of example, but this works.
// Listen for geocoding result
// This works the first time through, but fails if the user searchs for a second address because queryRenderedFeatures is working with a smaller set of features
geocoder.on('result', function(e) {
//need to clear geojson layer and
userDistrictsGeoJson = {
"type": "FeatureCollection",
"features": []
};
map.getSource('single-point').setData(e.result.geometry);
//project to use (pixel xy coordinates instead of lat/lon for WebGL)
var point = map.project([e.result.center[0], e.result.center[1]]);
var features = map.queryRenderedFeatures(point, { layers: ['congress-old'] });
var filter = featuresOld.reduce(function(memo, feature){
// console.log(feature.properties);
memo.push(feature.properties.GEOID);
return memo;
}, ['in', 'GEOID']);
map.setFilter('user-congress-old', filter);
var userCongressOldGeoJson = map.querySourceFeatures('congressional-districts', {
sourceLayer: 'congress_old',
filter: map.getFilter('user-congress-old')
});
userDistrictsGeoJson.features.push(userCongressOldGeoJson[0]);
var bbox = turf.bbox(userDistrictsGeoJson);
var bounds = [[bbox[0], bbox[1]], [bbox[2], bbox[3]]];
map.fitBounds(bounds, {
padding: 40
});
}); //geocoder result
}); //map load
So like I said, everything that runs on the geocodes 'result' event works the first time through, but it seems that on the second time through (user searches new address, but doesn't reload map) queryRenderedFeatures returns a smaller subset of features that doesn't include the tiles where the geocoder lands.
Any suggestions are much appreciated.
I ended up solving this by triggering the querying code once on 'moveend' event.
So now the syntax is:
geocoder.on('result', function(e){
map.once('moveend', function(){
.... rest of code
}
}
I thought I had tried this before posting the question, but seems to be working for me now.

Mapbox: Filtering out markers in a Leaflet Omnivore KML layer

I am exporting Google Directions routes as KML and displaying them on a Mapbox map by reading them with Omnivore and adding them to the map,
The Google KML stores each route as two Places (the start and end points) and one LineString (the route). In Mapbox I would like to show only the routes, that is to filter out the markers somehow. I'm displaying markers out of my own database and the Google markers clutter it up.
Here is my code. I change the styling of the LineStrings just to show that I can, but do not know what magic call(s) to make to not display the Points.
Thanks.
runLayer = omnivore.kml('data/xxxx.kml')
.on('ready', function() {
var llBnds = runLayer.getBounds();
map.fitBounds(llBnds);
this.eachLayer(function (layer) {
if (layer.feature.geometry.type == 'LineString') {
layer.setStyle({
color: '#4E3508',
weight: 4
});
}
if (layer.feature.geometry.type == 'Point') {
//
// Do something useful here to not display these items!!
//
}
});
})
.addTo(map);
Welcome to SO!
Many possible solutions:
Most straight forward from the code you provided, just use the removeLayer method on your runLayer Layer Group when you get a 'Point' feature.
Cleaner solution would be to filter out those features before they are even converted into Leaflet layers, through a custom GeoJSON Layer Group passed as 3rd argument of omnivore.kml, with a specified filter option:
var customLayer = L.geoJSON(null, {
filter: function(geoJsonFeature) {
// my custom filter function: do not display Point type features.
return geoJsonFeature.geometry.type !== 'Point';
}
}).addTo(map);
var runLayer = omnivore.kml('data/xxxx.kml', null, customLayer);
You can also use the style and/or onEachFeature options on customLayer to directly apply your desired style on your LineString.

Huge GIS data need to show on map using leaflet

I have a huge GIS data in db. I am fetching this data and converting it into geoJson file before viewing the map. I am fetching all of the data to meet the requirements, which is taking a lot of time. Is there any way to load data for a particular map window which user is viewing, and only loading respective GIS data for the same window using the leaflet library?
In my case, I am using Google Maps to show parking slots (GIS data) of a country on a map.
Yes, you need to write some piece of code to get only the specific data from server. See if below lines can help you
//whenever a drag or zoom finish, trigger the function to get new data
map.on('dragend', getData);//leaflet event
map.on('zoomend', getData);//leaflet event
function getData(){
//get the extent of your window
var minx = map.getBounds().getEast();
var miny = map.getBounds().getSouth();
var maxx = map.getBounds().getWest();
var maxy = map.getBounds().getNorth();
//trigger an ajax call to get data as per your extent
$.ajax({
url: 'url_like_a_php_service',
type: "get",
dataType: 'json',
async: false,
//send your extent to service to get specific data, like below
data: 'minx=' + minx + '&miny='+miny+ '&maxx='+maxx+ '&maxy='+maxy,
beforeSend: function () {
//you may write code here like showing a gif on map
},
complete: function (response) {
//pass data to another function to render on map
renderDataonMap();
}
});
}