Mapbox layer not updating after source update - reactjs

I'm using Redux state to update an array of coordinates in a Mapbox source. I initially check if there is a source with the id, if yes, I set the data of the source, if not I add the source to the map. When the redux state is changed, it triggers an effect which updates the coordinates of the features in the geojson object and uses setData to change the source. I've tried removing the layer, changing source and adding the layer, which just gave me the old layer (even though the source had indeed been updated). I also tried just updating the source alone and seeing if the layer would update dynamically, it did not.
Here is the code for the effect, which is triggered when the redux state is changed.
useEffect(() => {
const geoJsonObj = {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: []
}
};
for (let i = 0; i < (props.mapRoutes.length); i++) {
geoJsonObj.data.features.push({
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: props.mapRoutes[i].geometry.coordinates
}
});
};
const routeLayer = {
id: 'route',
type: 'line',
source: 'route',
layout: {
'line-join': 'round',
'line-cap': 'round'
},
paint: {
'line-color': '#ff3814',
'line-width': 5,
'line-opacity': 0.75
}
};
const jsonString = JSON.stringify(geoJsonObj);
const jsonObj = JSON.parse(jsonString);
if (props.mapRoutes.length) {
if (map.current.getSource('route')) {
map.current.getSource('route').setData(jsonObj);
} else {
map.current.addSource('route', jsonObj);
map.current.addLayer(routeLayer);
};
};
}, [props.mapRoutes]);
Neither of these worked and I am having trouble finding how to update a layer based on an updated source. Everything seems right when I inspect the source in the console, I just can't manage to update the layer on the map.
Any help would be appreciated.

I found the problem, I was using the original geoJson object for the setData method instead of the data entry, which was one level too high in the object. Just a simple error which was overlooked.

Related

Display HTML clusters with custom properties using react-map-gl (Mapbox)

I am trying to adapt the example Display HTML clusters with custom properties for react-map-gl.
I got basic clusters without custom styling working (adapted from Create and style clusters):
<ReactMapGL ref={mapRef}>
<Source id="poi-modal-geojson" type="geojson" data={pointsToGeoJSONFeatureCollection(points)}
cluster={true}
clusterMaxZoom={14}
clusterRadius={50}
>
<Layer {...{
id: 'clusters',
type: 'circle',
source: 'poi-modal-geojson',
filter: ['has', 'point_count'],
paint: {
'circle-color': [
'step',
['get', 'point_count'],
'#51bbd6',
100,
'#f1f075',
750,
'#f28cb1'
],
'circle-radius': [
'step',
['get', 'point_count'],
20,
100,
30,
750,
40
]
}
}} />
<Layer {...{
id: 'unclustered-point',
type: 'circle',
source: 'poi-modal-geojson',
filter: ['!', ['has', 'point_count']],
paint: {
'circle-color': '#11b4da',
'circle-radius': 4,
'circle-stroke-width': 1,
'circle-stroke-color': '#fff'
}
}} />
</Source>
</ReactMapGL>
Here, pointsToGeoJSONFeatureCollection(points: any[]): GeoJSON.FeatureCollection<GeoJSON.Geometry> is a function returning a GeoJSON (adapted from here).
However, I need more complex styling of markers and I am trying to adapt Display HTML clusters with custom properties without success so far. I mainly tried to adapt updateMarkers() and to call it inside useEffect():
const mapRef: React.Ref<MapRef> = React.createRef();
const markers: any = {};
let markersOnScreen: any = {};
useEffect(() => {
const map = mapRef.current.getMap();
function updateMarkers() {
const newMarkers: any = {};
const features = map.querySourceFeatures('poi-modal-geojson');
// for every cluster on the screen, create an HTML marker for it (if we didn't yet),
// and add it to the map if it's not there already
for (const feature of features) {
const coords = feature.geometry.coordinates;
const props = feature.properties;
if (!props.cluster) continue;
const id = props.cluster_id;
let marker = markers[id];
if (!marker) {
let markerProps = {
key: 'marker' + id,
longitude: coords[0],
latitude: coords[1],
className: 'mapboxgl-marker-start'
}
const el = React.createElement(Marker, markerProps, null),
marker = markers[id] = el;
}
newMarkers[id] = marker;
if (!markersOnScreen[id]) {
// TODO re-add
// marker.addTo(map);
}
}
// for every marker we've added previously, remove those that are no longer visible
for (const id in markersOnScreen) {
if (!newMarkers[id]) delete markersOnScreen[id];
}
markersOnScreen = newMarkers;
}
// after the GeoJSON data is loaded, update markers on the screen on every frame
map.on('render', () => {
if (!map.isSourceLoaded('poi-modal-geojson')) return;
updateMarkers();
});
}, [points]);
Unfortunately, the Marker created using React.createElement() isn't displayed I am not sure what is the right approach to create Marker elements in updateMarkers() or if my approach is completely wrong.
There is a great article on marker clustering which uses the supercluster and use-supercluster libraries and it makes clustering really easy not only for map box but for other map libraries as well, you can find it here.
You just have to convert your points into GeoJSON Feature objects in order to pass them to the useSupercluster hook and for the calculations to work. It will return an array of points and clusters depending on your current viewport, and you can map through it and display the elements accordingly based on the element.properties.cluster flag.
The properties property of the GeoJSON Feature object can be custom so you can pass whatever you need to display the markers later on when you get the final cluster array.

Adding observable object inside an array

I am having such a difficulty inserting observable into an array. What am I doing wrong here..
app.component.ts
const secondNavList = [];
this.appService.issuerList$.subscribe(iss => {
iss.forEach(value => {
console.log(value) //prints {name: 'A', id:'1'} {name: 'B', id:'2'}
secondNavList.push({
config: {
label: value.name
id: value.id
},
type: 'button'
});
});
};
console.log(secondNavList) // prints []
//But I want
//(2)[{...}.{...}]
appService.ts
get issuerList$(): Observable<Issuer[]>{
return this._issuerList.asObservable();
}
getIssuerList(){
const url = DBUrl
this.httpService.getData(url).subscribe((data:any[]) => {
let issuerList = [];
data.forEach(x=>{
issuerList.push(<Issuer>{name: x.issuerName, id: x.issuerId.toString()});
});
this._issuerList.next(issuerList)
})
}
Although inside my secondNavList, it contains data but I can't access it.
The fundamental issue you have is that you're trying to display the value of secondNavList before it is actually set in the subscriber. The rxjs streams are asynchronous, which implies that the the callback inside the subscribe method that appends to the list will get executed at some unknown point after subscribe is executed.
More importantly, I'd recommend that you try to take advantage of the map operator and array.map method, as well as the asyncronous pipes.
appService.ts
readonly issueUpdateSubject = new Subject<string>();
readonly issuerList$ = this.issueUpdateSubject.pipe(
switchMap(url => this.httpService.getData(url)),
map((data: any[]) => data.map(x => ({ name: x.issuerName, id: x.issuerId.toString() }))),
shareReplay(1)
);
getIssuerList() {
this.issueUpdateSubject.next(DBUrl);
}
app.component.ts
readonly secondNavList$ = this.appService.issuerList$.pipe(
map(iss => iss.map(value => ({
config: { label: value.name, id: value.id },
type: 'button'
}))
);
In the appService, instead of having an observable update a subject, I just had a subject emit update requests. Then instead of having to convert the subject to an observable, it just is an observable.
The shareReplay operator will share the most recently emitted list to any new subscribers.
Instead of appending to new arrays, I just use the array.map method to map each array element to the new desired object.
Instead of creating new array outside of the observable, and setting them in subscribe, I use the map operator to stream the latest instances of the arrays.
I find the more comfortable I got with rxjs the less I actually set the values of streams to instances of variables and rarely call subscribe - I just connect more and more streams and there values are used in components via async pipes. It's hard to get your head around it at first (or after a year) of using rxjs, but it's worth it in the end.
The error is because the observable value is an object array, and you want to add this into a simple object.
Try this.
const secondNavList = [];
this.appService.issuerList$.subscribe(iss => {
iss.forEach(value => {
console.log(value) //prints {name: 'A', id:'1'} {name: 'B', id:'2'}
value.forEach(v => {
secondNavList.push({
config: {
label: v.name,
id: v.id
},
type: 'button'
});
});
});
};
console.log(secondNavList) // prints []

Formatting data from a database in TypeScript

I am having trouble with writing the following method on an Angular class. I don't know how to add values from arrayId to the data array in the series object.
getChartOptions() {
const arrayId=[];
const arrayTimestamp=[];
const arrayData=[];
const arrayData2=[];
var i=0;
this.httpClient.get<any>('http://prod.kaisens.fr:811/api/sleep/?deviceid=93debd97-6564-454b-be33-35bd377a2563&startdate=1612310400000&enddate=1614729600000').subscribe(
reponse => {
this.sleeps = reponse;
this.sleeps.forEach(element => { arrayId.push(this.sleeps[i]._id),arrayTimestamp.push(this.sleeps[i].timestamp),arrayData.push(this.sleeps[i].data[18]),arrayData2.push(this.sleeps[i].data[39])
i++;
});
console.log(arrayId);
console.log(arrayTimestamp);
console.log(arrayData);
console.log(arrayData2);
}
)
return {
series: [{
name: 'Id',
data: [35, 65, 75, 55, 45, 60, 55]
}]
}
}
I have two main pieces of advice for you:
Know the types of that data that you are dealing with.
Get familiar with all of the various array methods.
get<any>() is not a helpful type. If you understand what the response is then Typescript can help ensure that you are handling it correctly.
I checked out the URL and it looks like you get an array of objects like this:
{
"_id": 4,
"device_id": "93debd97-6564-454b-be33-35bd377a2563",
"timestamp": 1612310400000.0,
"data": "{'sleep_quality': 1, 'sleep_duration': 9}"
},
That data property is not properly encoded as an object or as a parseable JSON string. If you control this backend then you will want to fix that.
At first I thought that the data[18] and data[39] in your code were mistakes. Now I see that it as attempt to extract values from this malformed data. Accessing by index won't work if these numbers can be 10 or more.
The type that you have now is:
interface DataPoint {
_id: number;
device_id: string;
timestamp: number;
data: string;
}
The type that you want is:
interface DataPoint {
_id: number;
device_id: string;
timestamp: number;
data: {
sleep_quality: number;
sleep_duration: number;
}
}
You can type the request as this.httpClient.get<DataPoint[]>( and now you'll get autocomplete on the data.
It looks like what you are trying to do is basically to convert this from one array of rows to a separate array for each column.
You do not need the variable i because the .forEach loop handles the iteration. The element variable in the callback is the row that you want.
this.sleeps.forEach(element => {
arrayId.push(element._id);
arrayTimestamp.push(element.timestamp);
arrayData.push(element.data[18]);
arrayData2.push(element.data[39]);
});
The .forEach loop that you have now is efficient because it only loops through the array once. A .map for each column is technically less efficient because we have to loop through separately for each column, but I think it might make the code easier to read and understand. It also allows Typescript to infer the types of the arrays. Whereas with an empty array you would need to annotate it like const arrayId: number[] = [];.
const mapData = (response: DataPoint[]) => {
return [{
name: 'Id',
data: response.map(element => element._id)
}, {
name: 'Timestamp',
data: response.map(element => element.timestamp)
}, {
name: 'Sleep Quality',
data: response.map(element => parseInt(element.data[18])) // fix this
}, {
name: 'Sleep Duration',
data: response.map(element => parseInt(element.data[39])) // fix this
}]
}
The HTTP request is asynchronous. If you access your array outside of the subscribe callback then they are still empty. I'm not an angular person so this part I'm unsure of, but I think that you want to be updating a property on your class instead of returning the value?
Just follow this piece of code:
series: [{
name: 'Id',
data: arrayId
}]

Apollo cache.modify update slower than setState while using in React Beautiful Drag and Drop

I'm fetching a query, and modifying the order of the list in it, using cache.modify on the drag end.
This does modify cache, as it should but, it takes milliseconds to do that.
How to reproduce the issue:
I'm using react beautiful dnd, to make drag-n-drop card.
It provides onDragEnd handler, where we can specify what happens when the user stops dragging.
In this case, I want to reorder the list on the drag end.
Cache modify:
cache.modify({
id: cache.identify(data.pod),
fields: {
stories(existingStoriesRefs, { readField }) {
return reorder(existingStoriesRefs, sourceIndex, destinationIndex);
},
},
});
Reorder logic:
const reorder = (list: any[], startIndex: number, endIndex: number) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
This is correctly working and rendering using stories in setState.
But, instead of copying Apollo data, to a new state, I think it's better to directly modify the cache.
But, using cache.modify, it works but, rendering is kind of glitchy. It seems, it first renders the existing list and then, modify cache in the next render. The glitch is around less than a second, but visible to the user.
I fixed it using cache.modify inside, mutation, and using optimistic update.
moveStoryMutation({
variables: {
id: stories[sourceIndex].id,
sourceIndex,
destinationIndex,
},
optimisticResponse: {
__typename: "Mutation",
moveStory: true,
},
update: (proxy) => {
proxy.modify({
id: proxy.identify(pod),
fields: {
stories(existingStoryRefs) {
return reorder(
existingStoryRefs,
sourceIndex,
destinationIndex
);
},
},
});

How to pass an array of coordinates to react-map-gl heatmap layer?

having some trouble reconciling the docs to my use-case. I got a little stuck trying to get openstreet maps into react using d3, and have been playing around with react-map-gl...great library that's pretty dialed-in! This library is built on top of d3 and openstreetmaps and uses a lot of d3 plugins...here's the example I am trying to replicate:
https://github.com/uber/react-map-gl/blob/5.0-release/examples/heatmap/src/app.js
In this example, the data where the coordinates live is in a geoJson file, and it is accessed in a method that looks like this (Copied and pasted from the link above...in this code they are using the d3-request plugin to fetch and parse through the geoJson file, which contains other data about earthquakes etc):
_handleMapLoaded = event => {
const map = this._getMap();
requestJson(
'https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson',
(error, response) => {
if (!error) {
// Note: In a real application you would do a validation of JSON data before doing anything with it,
// but for demonstration purposes we ingore this part here and just trying to select needed data...
const features = response.features;
const endTime = features[0].properties.time;
const startTime = features[features.length - 1].properties.time;
this.setState({
earthquakes: response,
endTime,
startTime,
selectedTime: endTime
});
map.addSource(HEATMAP_SOURCE_ID, {type: 'geojson', data: response});
map.addLayer(this._mkHeatmapLayer('heatmap-layer', HEATMAP_SOURCE_ID));
}
}
);
};
This is great if you are using GeoJson, and I have done this quite a bit to point d3 towards an object for US states, counties, or zipcodes...However what I am trying to do is much simpler! I have an array of data that I'm fetching, and passing down as props to this heatmap component, and it looks something like this:
[
{name: locationOne, latitude: 1.12345, longitude: -3.4567},
{name: locationTwo, latitude: 1.2345, longitude: -5.678},
...etc
]
So the question is, if I am not using geoJson, how do I tell the heatmap what coordinates to use? Any help is appreciated!!!
Even though the data in your array isn't geoJson, we can manipulate it into geoJSON. We can do this by creating a factory function to return valid geoJSON using the array data.
Once the data is converted to geoJSON it can be used as shown in the example you've found.
const rawData = [
{name: 'Feature 1', value: 2, latitude: 1.12345, longitude: -3.4567},
{name: 'Feature 2', value: 5, latitude: 1.2345, longitude: -5.678},
];
const makeGeoJSON = (data) => {
return {
type: 'FeatureCollection',
features: data.map(feature => {
return {
"type": "Feature",
"properties": {
"id": feature.name,
"value": feature.value
},
"geometry": {
"type": "Point",
"coordinates": [ feature.latitude, feature.longitude]
}
}
})
}
};
const myGeoJSONData = makeGeoJSON(rawData);
console.log(myGeoJSONData);

Resources