I am trying get Isochrone contours when the user clicks on a Marker,
The official Mapbox Documentation uses the built in Mapbox JS methods but I can't make it work with Leaflet JS
Here's what I have
function markerOnClick(lon, lat) {
const urlBase = "https://api.mapbox.com/isochrone/v1/mapbox/";
const profile = "cycling"; // Set the default routing profile
const minutes = 10; // Set the default duration
// Create a function that sets up the Isochrone API query then makes an fetch call
async function getIso() {
const query = await fetch(
`${urlBase}${profile}/${lon},${lat}?contours_minutes=${minutes}&polygons=true&access_token=${accessToken}`,
{ method: "GET" }
);
const data = await query.json();
map.getSource("iso").setData(data);
}
// From the official documentation
map.addSource("iso", {
type: "geojson",
data: {
type: "FeatureCollection",
features: [],
},
});
// I have tried to use the Leaflet geoJSON method to add it to the map
L.geoJSON("iso", {
type: "geojson",
data: {
type: "FeatureCollection",
features: [],
},
}).addTo(map);
// Can't find the substitute for this method in Leaflet
map.addLayer(
{
id: "isoLayer",
type: "fill",
// Use "iso" as the data source for this layer
source: "iso",
layout: {},
paint: {
// The fill color for the layer is set to a light purple
"fill-color": "#5a3fc0",
"fill-opacity": 0.3,
},
},
"poi-label"
);
// Make the API call
getIso();
}
I have tried to use the Leaflet method of adding GeoJSON to the map i.e. L.geoJSON but to no avail the mapbox GL JS methods I am trying to replace are
map.addLayer
map.addSource
any advice would be appreciated
L.geoJSON() expects a GeoJSON data structure, not a string. Do read https://leafletjs.com/reference#geojson .
For your particular case you probably want to do something like
const query = await fetch(`${urlBase}${profile}/${lon},${lat}?contours_minutes=${minutes}&polygons=true&access_token=${accessToken}`,{ method: "GET" });
const data = await query.json();
L.geoJson(data).addTo(map);
Related
Background
I am using deck.gl's PolygonLayer to render data that looks like this:
data.json:
{
"someKey": "someValue",
"spatialReference": {
"wkid": 23032,
},
"features": [
{
"attributes": {
"polygonName": "MY_POLYGON"
},
"geometry": {
"rings": [
[
[421334, 7240529], ...
],
[
[422656, 7250696], ...
]
]
}
}
]
}
Now, the problem is that decg.gl uses a latitude-longitude coordinate-system, which is different from what this polygon is expressed in.
Deck.GL documentation on rendering layers with different coordinate-systems
So, according to the documentation, deckGL renders each layer separately based on its coordinate system. Therefore, it was important to specify both coordinateOrigin and coordinateSystem props.
Understanding the coordinate system in data.json
So, as far as I understood, the spacialReference value in data.json represents an EPSG code. Using this website, I was able to find a value for the coordinateOrigin prop as [63.510617, 9.210989, 0]. As for the coordinateSystem prop, I used COORDINATE_SYSTEM.METER_OFFSETS. Here's the code:
PolygonLayer.tsx:
import React from "react";
import { COORDINATE_SYSTEM } from "#deck.gl/core/typed";
import { DeckLayer } from "#deck.gl/arcgis";
import { PolygonLayer } from "#deck.gl/layers/typed";
import MapView from "#arcgis/core/views/MapView";
import ArcGISMap from "#arcgis/core/Map";
import "#arcgis/core/assets/esri/themes/light/main.css";
export default function PolygonLayer({layerURL}) {
const mapRef = React.useState(null)
React.useEffect(() => {
fetch(layerURL)
.then(res => res.json())
.then(data => {
const blobURL = new Blob([JSON.stringify(data)], {type: "application/json",});
const url = URL.createObjectURL(blobURL); // this is needed for the layer
const layer = new PolygonLayer({
id: data["features"][0]["attributes"]["polygonName"], // correctly defined
data: url,
filled: true,
getLineWidth: 3,
getLineColor: [255, 255, 255, 0],
getFillColor: [234, 243, 221, 0],
coordinateOrigin: [63.510617, 9.210989, 0], // based on the explanation above
coordinateSystem: COORDINATE_SYSTEM.METER_OFFSETS,
getPolygon: (d) => {
console.log(d); // doesn't log anything
return d.features[0].geometry.rings;
},
});
const deckLayer = new DeckLayer({
"deck.layers": [layer],
});
const arcgisMap = new ArcGISMap({
basemap: "topo-vector",
layers: [deckLayer]
});
new MapView({
container: mapRef?.current,
map: arcgisMap,
center: data["features"][0]["geometry"]["rings"][0][0], // correctly defined
zoom: 9
});
})
.catch(err => console.log(err));
}, [layerURL]);
return <div ref={mapRef} style={{height: "90vh", width: "100%"}}></div>
}
The issue with this code
The problem with this code is that it doesn't render the layer (or the base map) and there's nothing logged in the console for the value of d; as mentioned in the code above.
Making sure the code works
Now, just a sanity check, I have used this url which returns polygons data in the standard LAT LONG format, without using coordinateOrigin or coordinateSystem props as in this example and it worked. So the code is ok rendering LAT LONG system, but breaks when using METERS_OFFSET as in the code provided.
Therefore
Have I figured out the coordinateOrigin correctly? And how can I use this (or another type of) layer to render this data correctly? Any help is appreciated and apologies for the long question!
I have created a MapboxGeocoder object in my code and I'm curious if I can access/use it to reverse geocode elsewhere in the code. This, in an effort to get a formatted address from coords.
I created an object like so:
const address = new MapboxGeocoder({
accessToken: mbat,
mapboxgl: mapboxgl,
container: 'geocoder',
countries: "us",
bbox: [-83.726807, 31.784217, -78.013916, 35.415915],
reverseGeocode: true,
}).on("result", function (result) {
console.log(result);
});
I also have the GeolocateControl object in my code and I'm creating it like so:
map.addControl(
new mapboxgl.GeolocateControl({
positionOptions: {
enableHighAccuracy: true
},
// When active the map will receive updates to the device's location as it changes.
trackUserLocation: true,
// Draw an arrow next to the location dot to indicate which direction the device is heading.
showUserHeading: true
}).on('geolocate', (e) => {
console.log(e)
})
)
My question is, is there a way to access the address object within the GeolocateControl event handler to reverse geocode? I am imagining something to this effect:
.on('geolocate', (e) => { console.log(address(e)) })
As long as your geocoder instance is in scope, you should be able to use it in the event handler from GeolocateControl
I am not 100% sure about the data object in the geolocate callback, so inspect it to see how to pull out the lng/lat coordinates.
const geocoder = new MapboxGeocoder(...)
map.addControl(
new mapboxgl.GeolocateControl({
...
}).on('geolocate', (data) => {
const geocoder.query(`${data.coords[0},${data.coords[1]}`)
})
)
Here I load the JSON file and plot them as network graph to visualize relationship between entity. The data has around 60 relationships and I plotted successfully with JavaScript code as follows:
fetch('data.json')
.then((response) => response.json())
.then((jsonData) => {
const dataSample = JSON.parse(jsonData);
const nodes = dataSample.relation.map((relation) => ({
id: relation.target_relation,
relation_type: relation.relation_type,
}));
nodes.push({
id: dataSample.party_source,
relation_type: '-',
});
const edges = dataSample.relation.map((relation) => ({
from: dataSample.party_source,
to: relation.target_relation,
relation_type: relation.relation_type,
}));
// graph data
const data = {
nodes,
edges,
};
const chart = anychart.graph(data);
// node configuration
const configNodes = chart.nodes();
configNodes.normal().height(20);
configNodes.hovered().height(25);
configNodes.tooltip().useHtml(true);
configNodes.tooltip().format(`Party ID: {%id}`);
// edge configuration
const configEdges = chart.edges();
configEdges.labels().enabled(true);
configEdges.labels().format('{%relation_type}');
configEdges.labels().fontSize(12);
configEdges.labels().fontColor('black');
configEdges.labels().fontWeight(500);
configEdges.tooltip().useHtml(true);
configEdges
.tooltip()
.format(`Party Source: {%from}<br>Party Target: {%to}`);
configEdges.arrows({
enabled: true,
size: 8,
});
configEdges.stroke({
color: '#7998FF',
thickness: '1.5',
});
chart.listen('mouseOver', function (e) {
// change the cursor style
document.body.style.cursor = 'pointer';
});
chart.listen('mouseOut', function (e) {
// set the default cursor style
document.body.style.cursor = 'auto';
});
// chart behaviour
chart.container('container');
chart.draw();
});
Unfortunately, I got each node on the network graph overlapped or not properly separated between nodes like picture below:
How to add spacing between nodes in order to avoid the overlapping, I have been searching on the documentation for the network graph, but not found any API function to perform that. Is it supposed to be a small sized data to produce a proper network graph?
Looking at their examples in this playground they are using this to influence the layout. Have you tried playing around with the iterationcount?
// set chart layout settings
chart.layout({ iterationCount: 0 });
Source
https://playground.anychart.com/gallery/src/Network_Graph/Radial_Graph
I am using Chart.js 3.5 and Vue 3.
I was successfully able to draw a chart, and I am trying to trigger a data change, inside a Vue method. Unfortunately, I encounter the following issue: "Uncaught TypeError: Cannot set property 'fullSize' of undefined".
Edit2: Added a missed }. Code should now be runnable
MyChart.vue:
<template>
<canvas id="chartEl" width="400" height="400" ref="chartEl"></canvas>
<button #click="addData">Update Chart</button>
</template>
<script>
import Chart from 'chart.js/auto';
export default {
name: "Raw",
data() {
return {
chart: null
}
},
methods: {
createChart() {
this.chart= new Chart(this.$refs["chartEl"], {
type: 'doughnut',
data: {
labels: ['VueJs', 'EmberJs', 'ReactJs', 'AngularJs'],
datasets: [
{
backgroundColor: [
'#41B883',
'#E46651',
'#00D8FF',
'#DD1B16'
],
data: [100, 20, 80, 20]
}
]
},
options: {
plugins: {}
}
})
},
addData() {
const data = this.chart.data;
if (data.datasets.length > 0) {
data.labels.push('data #' + (data.labels.length + 1));
for (var index = 0; index < data.datasets.length; ++index) {
data.datasets[index].data.push(123);
}
// Edit2: added missed }
this.chart.update(); } // this line seems to cause the error}
}
},
mounted () {
this.createChart()
},
}
</script>
Edit1: Adding the following to the options makes the chart update successfully, but the error is still present and the animation does not work. The chart flickers and displays the final (updated) state. Other animations, such as hiding/showing arcs do not seem to be afected
options: {
responsive: true,
}
Edit3: Adding "maintainAspectRatio:false" option seems to again stop chart from updating (the above mentioned error is still present)
By walking through the debugger, the following function from 'chart.esm.js' seems to be called successfully a few times, and then error out on last call:
beforeUpdate(chart, _args, options) {
const title = map.get(chart); // this returns null, which will cause the next call to error with the above mentioned exception.
layouts.configure(chart, title, options);
title.options = options;
},
//////////////////////
configure(chart, item, options) {
item.fullSize = options.fullSize;
item.position = options.position;
item.weight = options.weight;
},
This may be a stale post but I just spent several hours wrestling with what seems like the same problem. Perhaps this will help you and/or future people with this issue:
Before assigning the Chart object as an attribute of your Vue component, call Object.seal(...) on it.
Eg:
const chartObj = new Chart(...);
Object.seal(chartObj);
this.chart = chartObj;
This is what worked for me. Vue aggressively mutates attributes of objects under its purview to add reactivity, and as near as I can tell, this prevents the internals of Chart from recognising those objects to retrieve their configurations from its internal mapping when needed. Object.seal prevents this by barring the object from having any new attributes added to it. I'm counting on Chart having added all the attributes it needs at init time - if I notice any weird behaviour from this I'll update this post.
1 year later, Alan's answer helps me too, but my code failed when calling chart.destroy().
So I searched and found what seems to be the "vue way" of handling it: markRaw, here is an example using options API:
import { markRaw } from 'vue'
// ...
export default {
// ...
beforeUnmount () {
if (this.chart) {
this.chart.destroy()
}
},
methods: {
createChart() {
const chart = new Chart(this.$refs["chartEl"], {
// ... your chart data and options
})
this.chart = markRaw(chart)
},
addData() {
// ... your update
this.chart.update()
},
},
}
I have a mapbox map, initialized with the outdoors-v9 style (tried other styles, same behavior). When I add a layer to the map - a marker or a geojson source and zoom the map, the style changes or breaks, I'm not sure which.
This is the map before the zoom
and after the zoom
here are the functions that init the map and add markers
mapboxgl.accessToken = "pk.*******";
buildMap: function() {
const _self = this;
_self.map = new mapboxgl.Map({
container: "map",
style: "mapbox://styles/mapbox/outdoors-v9",
center: [-95.712891, 37.09024],
zoom: 3
});
_self.map.on('load', function() {
_self.map.addSource('route', {
'type': 'geojson',
'data': {
"type": "FeatureCollection",
"features": []
}
});
_self.map.addLayer({
'id': 'route',
'source': 'route',
'type': 'line',
'layout': {
'line-join': 'round',
'line-cap': 'round'
},
'paint': {
'line-color': '#47576A',
'line-width': 3
}
});
});
}
...
const coords = [addressData.longitude, addressData.latitude];
const marker = new mapboxgl.Marker().setLngLat(coords).addTo(this.map);
I am using Vue.js to render the map. Mapbox version v0.45.0
Any help or leads are highly appreciated
Vue data() properties are reactive, they have getters and setters, so, when loading map object or adding vector tiles layer (geojson), Vue tries to add getters & setters to the map & map.layers which causes vue & vue-dev-tools to crash and mess up the map.
If you enable any raster layer, it would work successfully because raster tiles are loaded via the mapbox.css whereas vector tiles being geojson, are added to the map object.
Easiest solution would be to define a non-reactive variable in vue and then re-use it everywhere.
// edit: A correct/recommended way to set non-reactive data: GitHub link
Seems the issue was related with the fact that I'm pushing the marker instance to an observable (a vuejs data field). After pushing the marker instance to an array, the issue disappeared. This comment doesn't really answer why this happens, but hope it helps someone else that might face the same issue
I just faced this issue and realized that I didn't follow the documentation exactly as it was described (jumped right on to coding without reading properly). And the documentation says:
Storing Map object
Take note that it's generally bad idea to add to Vuex or component's
data anything but primitive types and plain objects. Vue adds getters
and setters to every property, so if you add Map object to Vuex store
or component data, it may lead to weird bugs. If you want to store map
object, store it as non-reactive property like in example below.
The problem was that I had also registered "map" inside the "data" object of my Vue component. But in the example code it's not declared in data, only in the "create" function.
https://soal.github.io/vue-mapbox/guide/basemap.html#map-loading
After hours spent on this problem, here is my working solution to access map instance from a store (thanks to https://github.com/vuejs/vue/issues/2637#issuecomment-331913620):
const state = reactive({
map: Object.freeze({ wrapper: /* PUT THE MAP INSTANCE HERE */ });
});
Here is an example with Vue Composition Api:
index.js
import { reactive, computed } from "#vue/composition-api";
export const state = reactive({
map: null
});
export const setMap = (map) => {
state.map = Object.freeze({ wrapper: map});
};
export const getMap = computed(() => state.map.wrapper);
export const initMap = (event) => {
setMap(event.map);
// now you can access to map instance from the "getMap" getter!
getMap.value.addSource("satellite-source", {
type: "raster",
url: "mapbox://mapbox.satellite",
});
getMap.value.addLayer({
id: "satellite-layer",
type: "raster",
source: "satellite-source"
});
};
App.vue
<template>
<MglMap :accessToken="..." :mapStyle="..." #load="onMapLoaded" />
</template>
<script>
import { defineComponent } from "#vue/composition-api";
import { MglMap } from "vue-mapbox";
import { initMap } from "./index.js";
export default defineComponent({
components: {
MglMap
},
setup() {
const onMapLoaded = (event) => {
initMap(event);
}
return { onMapLoaded };
}
});
</script>
I've got the same error.
This happens if you either put the map or the marker on an reactive vue.js instance.
Short and quick answer.
Explanation is similar to #mlb's answer. So you freeze the object to prevent the map from disorientated and for any actions done to the map, call back the data with an extra Object key which in case is 'wrapper'.
<template><MglMap :accessToken="..." :mapStyle="..." #load="onMapLoaded" /></template>
<script>
methods: {
onMapLoaded(event) {
this.mapboxEvent = Object.freeze({wrapper: event.map});
},
panMap(event) {
this.mapboxEvent.wrapper.panTo([lng, lat], {duration: 1000, zoom: 14});
}
}
</script>