Clustering Points in Mapbox - javascript

I have done to create people distribution map including its photo as icon. And now, I want to try clustering them. But, I confused to implement them if reffering to Mapbox Docs (https://docs.mapbox.com/mapbox-gl-js/example/cluster/), because I looping the point to show each photo of the point. Do someone have an idea to this topic? This my code:
$(document).ready(() => {
$.ajax({
type: "GET",
//YOUR TURN: Replace with csv export link
url: `${google_sheet_name}/gviz/tq?tqx=out:csv&sheet=${sheet_name}`,
dataType: "text",
success: function (csvData) {
makeGeoJSON(csvData);
}
});
let makeGeoJSON = csvData => {
csv2geojson.csv2geojson(csvData, {
latfield: 'latitude',
lonfield: 'longitude',
delimiter: ','
}, (err, data) => {
var geo = {
'id': 'csvData',
'type': 'circle',
'source': {
'type': 'geojson',
'data': data
},
}
geo.source.data.features.forEach(marker => {
var el = document.createElement('img');
el.className = 'icon-image';
el.src = marker.properties.image_path
// make a marker for each feature and add it to the map
new mapboxgl.Marker(el)
.setLngLat(marker.geometry.coordinates)
.setPopup(new mapboxgl.Popup({ offset: 25 }) // add popups
.setHTML('<h3>' + marker.properties.name + '</h3><p>' + marker.properties.description + '</p>'))
.addTo(map)
})
let UseBbox = () => {
let bbox = turf.bbox(data);
map.fitBounds(bbox, {
padding: 50
})
}
UseBbox()
});
};
});

Instead of rendering marker for each point, the example works with geojson data and layers. This gives better performance when you have more number of points.
The basic flow for your case would be something like
Add data to source to map as explained in example
Add 2 layers, one for rendering cluster icon, another for uncluster point
Clustered icon can be same as in example
Unclustered layer can use imagepath to render the user image
To dynamically load and render image use styleimagemissing event. Refer the example at https://docs.mapbox.com/mapbox-gl-js/example/add-image-missing-generated/
Note that adding images should be done synchronosly

Related

Filter Markers by Date in Leaflet

I built a photo map with leaflet js which contains geotagged photos. I managed to fetch the timestamp and coordinates of the pictures with a php script and now I want to filter the images by it's recording date. The metadata of the imported images contain information about Year, Year.Month and Year.Month.Day.
This is my leaflet setup:
var photoLayer = L.photo.cluster({ spiderfyDistanceMultiplier: 1.2 }).on('click', function (evt) {
evt.layer.bindPopup(L.Util.template('<span class="DatumPopup">Aufnahmedatum: {DateTimeOriginal} <span class="Uhrzeit">{Time}</span><img src="{url}" height="auto" width="100%"/>', evt.layer.photo), {
className: 'leaflet-popup-photo',
minWidth: 400
}).openPopup();
});
//Call the next function as soon as the page loads
window.onload = callForImages()
//Makes a request, loading the getimages.php file
function callForImages() {
//Create the request object
var httpReq = (window.XMLHttpRequest)?new XMLHttpRequest():new ActiveXObject("Microsoft.XMLHTTP");
//When it loads,
httpReq.onload = function() {
//Convert the result back into JSON
var result = JSON.parse(httpReq.responseText);
//Load the images
loadImages(result);
}
//Request the page
try {
httpReq.open("GET", "getphotos.php", true);
httpReq.send(null);
} catch(e) {
console.log(e);
}
}
//Generates the images and sticks them into the photolayer
function loadImages(images) {
var photos = [];
//Loop over the images
for(var i = 0; i < images.length; i++) {
photos.push({
lat: images[i].lat,
lng: images[i].lng,
url: images[i].filename,
DateTimeOriginal: images[i].DateTimeOriginal,
Year: images[i].Year,
YearMonth: images[i].YearMonth,
Time: images[i].Time,
//If you have thumbnails, switch the comments on the following lines.
thumbnail: images[i].filename
//thumbnail: images[i].thumbnail
});
}
photoLayer.add(photos).addTo(map);
//Add the photos to the map
map.fitBounds(photoLayer.getBounds());
//Zoom the map to the photos
}
}).addTo( map );
I have a hard time wrapping my head around how to apply the filter with the layer control. Since I used leaflet.photo to import the pictures, I don't even know hot wo apply a classic layer control to this project. Anyone who could give me a hint in the right direction?

How to use one L.Shapefile/zip file Object but change the onEachFeature for each layer?

The current problem I am experiencing is that i have multiple tileLayers that contain a shape file. Each tile layer represents a different data-set based on some variable and changes the colors accordingly.
I can get this to work if I create three separate Objects(L.shapefile("", {})...) and place each into it's corresponding layer. The problem is that my shapefile is fairly large at 12MB. When I do it this way it downloads the file 3 times.
How would I go about downloading the zip file once, then creating 3 instances from that download?
shpfile = new L.Shapefile('/mydata/shpfile.zip', {
onEachFeature: function (feature, layer) {
console.log(feature, layer)
if (feature.properties) {
var data_name = feature.properties.Name;
val = -1
if (data)
val = data[days+'DAY']
if (val==-1)
val="No Data";
var tooltip_Html = data_name + "<hr>" + days + " " + val;
layer.bindPopup(tooltip_Html, {
maxHeight: 200
});
layer.prop = {data_name , days};
layer.setStyle({fillColor : getColor(val),
color: "black",
weight: 1,
opacity: 1,
fillOpacity: 0.3
});
layer.on("click", layerClick);
if (vw>768) {
layer.on("mouseover", function () {
info.update(layer.prop);
layer.bindTooltip('<b>'+layer.prop.loc + '</b> Name<br>' + layer.prop.val);
});
layer.on("mouseout", function () {
info.update();
});
}
}
}});
shpfile.once("data:loaded", function() {
console.log("Shape File Downloaded! "+ days + ' days');
/* Tried this method of creating oneachfeature dynamically
shpfile.onEachFeature(function(feature, layer) {
var layerName;
});
*/
});
L.tileLayer(mapboxUrl, {id: 'mapbox.streets', attribution: mbAttr}).addTo(shpfile)```
Let me quote the Leaflet.shapefile readme:
usage:
new L.Shapefile(arrayBuffer or url[,options][,importUrl]);
L.shapefile(arrayBuffer or url[,options][,importUrl]);
So one can provide either the URL to a shapefile, or an ArrayBuffer with the contents of the zipped shapefile.
So now the question is: how to create an arrayBuffer of an external file? There are several approaches, but I'm partial to the fetch API and the Response.arrayBuffer() functionality, e.g.
fetch(url)
.then(function(response){
return response.arrayBuffer();
}).then(function(buf){
L.shapefile(buf, options1).addTo(map);
L.shapefile(buf, options2).addTo(map);
L.shapefile(buf, options3).addTo(map);
});

Using buildLocationList() with an external GeoJSON file in Mapbox GL JS

I am working on a web map that just requires flying to different locations on click, as in this Mapbox GL example (https://docs.mapbox.com/help/tutorials/building-a-store-locator/#getting-started). However, I am trying to load GeoJSON features from an external file and I can get the points to appear but not the list items. Basically, I cannot figure out how to get the list to build since (buildLocationList(stores);) using this method. Is there a way to set the external GeoJSON file's variable name as 'stores.' Any help would be appreciated.
var stores = "https://raw.githubusercontent.com/aarontaveras/Test/master/sweetgreen.geojson";
map.on('load', function () {
// Add the data to your map as a layer
map.addLayer({
id: 'locations',
type: 'symbol',
// Add a GeoJSON source containing place coordinates and information.
source: {
type: 'geojson',
data: stores
},
layout: {
'icon-image': 'circle-15',
'icon-allow-overlap': true,
}
});
// Initialize the list
buildLocationList(stores);
});
function buildLocationList(data) {
for (i = 0; i < data.features.length; i++) {
// Create an array of all the stores and their properties
var currentFeature = data.features[i];
// Shorten data.feature.properties to just `prop` so we're not
// writing this long form over and over again.
var prop = currentFeature.properties;
// Select the listing container in the HTML
var listings = document.getElementById('listings');
// Append a div with the class 'item' for each store
var listing = listings.appendChild(document.createElement('div'));
listing.className = 'item';
listing.id = "listing-" + i;
// Create a new link with the class 'title' for each store
// and fill it with the store address
var link = listing.appendChild(document.createElement('a'));
link.href = '#';
link.className = 'title';
link.dataPosition = i;
link.innerHTML = prop.address;
// Create a new div with the class 'details' for each store
// and fill it with the city and phone number
var details = listing.appendChild(document.createElement('div'));
details.innerHTML = prop.city;
if (prop.phone) {
details.innerHTML += ' · ' + prop.phoneFormatted;
}
I was able to easily load the data from a external source, but am still struggling to build the list.
var stores = 'https://raw.githubusercontent.com/aarontaveras/Test/master/sweetgreen.geojson';
map.on('load', function () {
map.addSource("locations", {
type: 'geojson',
data: stores
});
map.addLayer({
"id": "locations",
"type": "symbol",
"source": "locations",
"layout": {
'icon-image': 'circle-15',
'icon-allow-overlap': true,
}
});
});
Mapbox GeoJSON Sources data property can be a URL to a GeoJSON file, or inline GeoJSON. So you can fetch your GeoJSON data and pass it to the source directly and use it to build your locations list.
Consider example:
map.on('load', () => {
fetch(stores)
.then(response => response.json())
.then((data) => {
map.addSource("locations", {
type: 'geojson',
data: data
});
map.addLayer(...);
buildLocationList(data);
});
});

How can I create an ionic popover from a leafletjs polygon?

I am using Leaflet JS to draw polygons on a map.
I am able to draw the shapes, and to save the shapes to a database as GeoJSON like this:
.ts file
...
let layer = event.layer;
let geometry = layer.toGeoJSON();
this.drawLayer.addLayer(layer);
}
I call a method in the ngOnInit method to redraw the shapes:
drawPolygonShape(data) {
if (data.polygon.geometry) {
let shape = {
type: data.polygon.type,
geometry: {
type: data.polygon.geometry.type,
coordinates: data.polygon.geometry.coordinates
},
properties: {}
};
L.geoJSON(shape, {
onEachFeature: this.onEachFeature
}).addTo(this.myMap);
}
}
...
onEachFeature(feature, layer) {
layer.on('click', event => {
// layer.bindPopup('Hello World'); // Works great
let popover = this.popoverCtrl.create('MyComponent', { layer: layer });
popover.present();
});
}
I am importing MyComponent into the module file, so I know it is available. However, I am always getting the following error message:
Cannot read property 'create' of undefined
So, somewhere there is a timing or scope problem that is not correctly working with the click event.
It seems like a scope problem, as any component gives the same error. Any suggestions are greatly appreciated!
EDIT
Thank you to #ghybs I've tried adding this as a third argument to the click event, but I'm still getting the same error:
onEachFeature(feature, layer) {
layer.on('click', event => {
// layer.bindPopup('Hello World'); // Works great
let popover = this.popoverCtrl.create('MyComponent', { layer: layer });
popover.present();
}, this);
}
EDIT 2
Thank you #ghybs!
I'm curious about your second suggestion - I am setting up the map like this:
let mapOptions: any = {
position: 'topright',
draw: {
polyline: false,
...
}
}
...
let drawControl = new L.Control.Draw(mapOptions);
this.myMap.addControl(drawControl);
this.myMap.on(L.Draw.Event.CREATED, this.onCreatePolygon.bind(this));
The .bind(this) makes more sense now - onCreatePolygon is a method I am using to save the shape.
How do you suggest I delegate to the click event for each shape?
this.myMap.on(L.Draw.Event.CLICK, this.MYCLICKHANDLER.bind(this));
(it's obvious I'm not familiar working with this so I appreciate all of your time.)
In your case you have a double context issue since you also pass your onEachFeature method that will get invoked with a this context that is already different from your class instance.
You could do onEachFeature: this.onEachFeature.bind(this) together with your layer.on("click", cb, this)
You could also use event delegation by attaching the listener on your GeoJSON Layer Group instead of each individual feature, so that you get rid of one of the context:
var geoJsonData = {
"type": "Point",
"coordinates": [2.35, 48.86]
};
class Component {
constructor(mapId) {
this.myMap = L.map(mapId).setView([48.86, 2.35], 11);
L.geoJSON(geoJsonData).addTo(this.myMap).on("click", event => {
console.log(event.target); // this is the GeoJSON Layer Group.
console.log(event.layer); // this is the individual child feature.
let popover = this.popoverCtrl.create('MyComponent');
popover.present();
}, this); // Not even needing the 3rd arg since you use a fat arrow function, which does not have its own context.
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(this.myMap);
}
}
Component.prototype.popoverCtrl = { // Dummy popoverCtrl
create(content) {
dummyPopoverContent = content;
return {
present() {
alert(dummyPopoverContent);
},
};
},
};
var dummyPopoverContent = '';
new Component('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>
See also start a function when click on circle - leaflet
As for your last question, with the 1st remark above you could re-write:
this.myMap.on(L.Draw.Event.CREATED, this.onCreatePolygon.bind(this));
into:
this.myMap.on(L.Draw.Event.CREATED, this.onCreatePolygon, this);
Event delegation does not seem relevant in this case since you already attach a single listener on your map, not on individual layers.

Draggable issue when loading Multiple Images in Kinetic JS

$.ajax({
type: 'get',
url: myUrl,
success: function(data) {
//alert("ajax");
$.each(data, function(idx, obj) {
createImage(obj.id, obj.mPosX, obj.mPosY);
});
},
contentType: "application/json",
dataType: 'json'
});
function createImage(mId, curX, curY) {
var layer2 = new Kinetic.Layer();
var imageObj = new Image();
imageObj.src = 'img/pawn.png';
imageObj.onload = function() {
pImage[mId] = new Kinetic.Image({
x: curX,
y: curY,
image: imageObj,
draggable:true,
id: pImage[mId]
});
layer2.add(pImage[mId]);
layer2.setScale(cur_scale);
stage.add(layer2);
};
}
I am trying to add multiple images with different position from a JSON file, I am successful in placing the images in exact location, But the problem is if i try to drag the image it position itself at top of the canvas.
Any idea why it is behaving like this.?
right now you are creating a new layer for every single image. That is not how KJS works.
You first create a stage, then create a layer and then add all the images to this one layer.
Maybe this will solve the problem.

Categories