display openlayers infowindow in React - reactjs

I am trying to display an info window (overlay popup) when the user clicks on one of the markers displayed on the map. Here's the code:
export const Home = () => {
const { centerPoint, zoomValue, testSites } = useContext(AppContext);
const [layer, setLayer] = useState<VectorLayer>(new VectorLayer({}));
const [popup, setPopup] = useState<Overlay>(new Overlay({}));
const popupRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const [map] = useState(
new Map({
interactions: defaultInteractions().extend([
new DragRotateAndZoom()
]),
controls: defaultControls().extend([
new ScaleLine({
units: 'imperial'
})
]),
target: '',
layers: [new TileLayer({
source: new SourceOSM()
})],
view: new View({
center: fromLonLat([centerPoint.longitude, centerPoint.latitude]),
zoom: zoomValue
})
})
);
useEffect(() => {
map.setTarget('map');
map.on('click', (event) => {
const feature = map.forEachFeatureAtPixel(event.pixel, (feature) => {
return feature;
});
if (feature) {
popup.setPosition(event.coordinate);
if (contentRef.current) {
contentRef.current.innerHTML = '<p>' + feature.getProperties().name + '</p>';
}
}
});
map.on('pointermove', (event) => {
if (!event.dragging) {
map.getTargetElement().style.cursor = map.hasFeatureAtPixel(map.getEventPixel(event.originalEvent)) ? 'pointer' : '';
}
});
setPopup(new Overlay({
element: popupRef.current,
positioning: 'bottom-center' as OverlayPositioning,
stopEvent: false,
offset: [9, 9],
}));
}, [map]);
useEffect(() => {
map.addOverlay(popup);
}, [popup, map]);
useEffect(() => {
if (testSites.length) {
const features: Feature[] = [];
testSites.forEach((testSite: TestSite) => {
const feature = new Feature({
geometry: new Point(fromLonLat([testSite.longitude, testSite.latitude])),
name: testSite.name,
address: testSite.address,
city: testSite.city,
state: testSite.state,
notes: testSite.notes
});
feature.setStyle(new Style({
image: new Icon({
src: 'images/site.png'
})
}));
features.push(feature);
});
setLayer(new VectorLayer({
source: new VectorSource({
features: features
})
}));
if (layer.getProperties().source !== null) {
map.addLayer(layer);
}
}
map.getView().animate({zoom: zoomValue}, {center: fromLonLat([centerPoint.longitude, centerPoint.latitude])}, {duration: 1000});
}, [centerPoint, zoomValue, map, testSites]);
return (
<div className="map-wrapper">
<div id="map"></div>
<div id="popup" className="map-popup" ref={popupRef}>
<div id="popup-content" ref={contentRef}></div>
</div>
</div>
);
};
Pretty much everything works fine except for displaying the info window on feature icon click. From what I can tell the positioning on click is being applied to a different div, not the one that contains . See screenshot below. Any help would be appreciated. Thanks.

Ok, I solved it. I assigned an id to my overlay and then referenced it inside the on click event function.
setPopup(new Overlay({
id: 'info',
element: popupRef.current,
positioning: 'bottom-center' as OverlayPositioning,
stopEvent: false,
offset: [9, 9],
}));
......
if (feature) {
map.getOverlayById('info').setPosition(event.coordinate);
if (contentRef.current) {
contentRef.current.innerHTML = '<p>' + feature.getProperties().name + '</p>';
}
}

Related

ChartJS 3 Doesn't Show Data Until A Legend Is Clicked

I get some data from back-end to show. When I inspect element with React Developer Tools, I can see that data is there but not shown in production. ChartJS version is 3.8, not react-chartjs
I was having the same problem in development, too, but solved it by setting a unique key with key={Math.random()}. In development build, it works just fine. Problem occurs in production. I deploy my app on Firebase.
I wait for data before rendering:
{isAnyFetching ? "Loading..." : <BarChart01 data={chartData} width={595} height={248} key={Math.random()} />}
I tried giving an array of zeroes until data is loaded to be sure chartData changed to trigger re-render by changing the state of the chart component. I also tried giving an extraKey prop and change it with useEffect to re-render again.
The whole chart component is:
function BarChart01({
data,
width,
height
}) {
const canvas = useRef(null);
const legend = useRef(null);
useEffect(() => {
const ctx = canvas.current;
// eslint-disable-next-line no-unused-vars
const chart = new Chart(ctx, {
type: 'bar',
data: data,
options: {
layout: {
padding: {
top: 12,
bottom: 16,
left: 20,
right: 20,
},
},
scales: {
y: {
grid: {
drawBorder: false,
},
ticks: {
maxTicksLimit: 6,
callback: (value) => formatValue(value),
},
},
x: {
type: 'time',
time: {
parser: 'MM-DD-YYYY',
unit: 'month',
displayFormats: {
month: 'MMM YY',
},
},
grid: {
display: false,
drawBorder: false,
},
},
},
plugins: {
legend: {
display: true,
},
tooltip: {
callbacks: {
title: () => false, // Disable tooltip title
label: (context) => formatValue(context.parsed.y),
},
},
},
interaction: {
intersect: false,
mode: 'nearest',
},
animation: {
duration: 500,
},
maintainAspectRatio: false,
resizeDelay: 200,
},
plugins: [{
id: 'htmlLegend',
afterUpdate(c, args, options) {
const ul = legend.current;
if (!ul) return;
// Remove old legend items
while (ul.firstChild) {
ul.firstChild.remove();
}
// Reuse the built-in legendItems generator
const items = c.options.plugins.legend.labels.generateLabels(c);
items.forEach((item) => {
const li = document.createElement('li');
li.style.marginRight = tailwindConfig().theme.margin[4];
// Button element
const button = document.createElement('button');
button.style.display = 'inline-flex';
button.style.alignItems = 'center';
button.style.opacity = item.hidden ? '.3' : '';
button.onclick = () => {
c.setDatasetVisibility(item.datasetIndex, !c.isDatasetVisible(item.datasetIndex));
c.update();
};
// Color box
const box = document.createElement('span');
box.style.display = 'block';
box.style.width = tailwindConfig().theme.width[3];
box.style.height = tailwindConfig().theme.height[3];
box.style.borderRadius = tailwindConfig().theme.borderRadius.full;
box.style.marginRight = tailwindConfig().theme.margin[2];
box.style.borderWidth = '3px';
box.style.borderColor = item.fillStyle;
box.style.pointerEvents = 'none';
// Label
const labelContainer = document.createElement('span');
labelContainer.style.display = 'flex';
labelContainer.style.alignItems = 'center';
const value = document.createElement('span');
value.style.color = tailwindConfig().theme.colors.slate[800];
value.style.fontSize = tailwindConfig().theme.fontSize['3xl'][0];
value.style.lineHeight = tailwindConfig().theme.fontSize['3xl'][1].lineHeight;
value.style.fontWeight = tailwindConfig().theme.fontWeight.bold;
value.style.marginRight = tailwindConfig().theme.margin[2];
value.style.pointerEvents = 'none';
const label = document.createElement('span');
label.style.color = tailwindConfig().theme.colors.slate[500];
label.style.fontSize = tailwindConfig().theme.fontSize.sm[0];
label.style.lineHeight = tailwindConfig().theme.fontSize.sm[1].lineHeight;
const theValue = c.data.datasets[item.datasetIndex].data.reduce((a, b) => a + b, 0);
const valueText = document.createTextNode(formatValue(theValue));
const labelText = document.createTextNode(item.text);
value.appendChild(valueText);
label.appendChild(labelText);
li.appendChild(button);
button.appendChild(box);
button.appendChild(labelContainer);
labelContainer.appendChild(value);
labelContainer.appendChild(label);
ul.appendChild(li);
});
},
}],
});
chart.update();
return () => chart.destroy();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
return (
<>
<div className="px-5 py-3">
<ul ref={legend} className="flex flex-wrap"></ul>
</div>
<div className="grow">
<canvas ref={canvas} width={width} height={height}></canvas>
</div>
</>
);
}
According to the code, it seems like
button.onclick = () => {
c.setDatasetVisibility(item.datasetIndex,!c.isDatasetVisible(item.datasetIndex));
c.update();
};
part in forEach loop is responsible of this update operation when I click on a label. So it somehow doesn't call update function in production as it should as useEffect listens to data prop.

Nivo line chart custom mesh layer

I have nivo line chart with gaps like this:
Gaps are covered by passing y/value: null in november and december in data series
Tooltip displays only on data points and this is correct, but I want add tooltip at November and December with explanation why there is no data.
The solution is to add custom layer 'mesh' which is responsible for displaying tooltips on line chart.
You have to declare custom layers in <ResponsiveLine component:
layers={[
'grid',
'markers',
'axes',
'areas',
'crosshair',
'lines',
'slices',
'points',
CustomMesh,
'legends',
]}
Create CustomMesh component:
const CustomMesh = (layerData: any) => {
const { showTooltipAt, hideTooltip } = useTooltip();
const handleMouseEnter = (point: any) => {
showTooltipAt(
layerData.tooltip({ point }),
[point.x + layerData.margin.left, point.y + layerData.margin.top],
'top'
);
};
const handleMouseMove = (point: any) => {
showTooltipAt(
layerData.tooltip({ point }),
[point.x + layerData.margin.left, point.y + layerData.margin.top],
'top'
);
};
const handleMouseLeave = (point: any) => {
hideTooltip();
};
const nullValuePoints = layerData.series.reduce((acc: any[], cur: any) => {
cur.data.forEach(({ data, position }: any) => {
if (data.y === null) {
const point = {
x: position.x,
y: 100, //whatever you want
data: {
x: data.x,
},
};
acc.push(point);
}
});
return acc;
}, []);
return (
<Mesh
nodes={[...layerData.points, ...nullValuePoints]}
width={layerData.width}
height={layerData.height}
onMouseEnter={handleMouseEnter}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
debug={layerData.debugMesh}
/>
);
};
When nullValuePoints are my custom points with no data
Import required packages:
import { Mesh } from '#nivo/voronoi';
import { useTooltip } from '#nivo/tooltip';
result:

Functional Component Not Re-rendering after data is updated

I am building a dashboard filled with Esri maps that are editable. The structure of the components is something like this:
<Dashboard>
<Visuals>
<EventsMap>
<PointsLayer/>
</EventsMap>
</Visuals>
</Dashboard>
When a user edits something inside of the Dashboards component(i.e. the color of the points on a map) the data does get passed through to PointsLayer which then should re-render and show the updated color, but only updates if I refresh the page. Is it because I don't have a render method? The PointsLayer component:
import {useState, useEffect} from 'react';
import {loadModules} from 'esri-loader';
import {getFormattedDate} from 'Lib/helpers';
import styles from './Summary.module.css';
import point from "#arcgis/core/geometry/Point";
const PointsLayer = (props) => {
const data = props.data;
const color = data?.color;
console.log(color)
const humanize = (str) =>{
let i, frags = str.split('_');
for (i=0; i<frags.length; i++) {
frags[i] = frags[i].charAt(0).toUpperCase() + frags[i].slice(1);
}
return frags.join(' ');
}
const pluralize = (val, word, plural = word + 's') => {
const _pluralize = (num, word, plural = word + 's') =>
[1, -1].includes(Number(num)) ? word : plural;
if (typeof val === 'object') return (num, word) => _pluralize(num, word, val[word]);
return _pluralize(val, word, plural);
};
const [graphic, setGraphic] = useState(null);
useEffect(() => {
loadModules(['esri/Graphic']).then(([Graphic]) => {
// Parse out the Lat-Long from each Event and the doc_count
for (let i = 0; i < data?.events.length; i++) {
const point = {
type: "point", // autocasts as new Point
longitude: data?.events[i]?.location.split(",")[1],
latitude: data?.events[i]?.location.split(",")[0]
};
// Create a symbol for rendering the graphic
const symbol = {
type: "simple-marker", // autocasts as new SimpleMarkerSymbol()
style: "circle", color: color, // Color Selected on popup
size: "12px", outline: {
color: [255, 255, 255], // White
width: 1.5
}
};
// Create attributes for popup
const attributes = {
watcherType: humanize(data?.events[i]?.doc_fields?.watcher_type),
eventCount: data?.events[i]?.doc_count,
plural: pluralize(data?.events[i]?.doc_count, 'Event'),
deviceName: data?.events[i]?.key,
lat: data?.events[i]?.location.split(",")[0],
long: data?.events[i]?.location.split(",")[1],
account: data?.events[i]?.doc_fields?.account_id,
address: data?.events[i]?.doc_fields['#service_address'],
meterId: data?.events[i]?.doc_fields?.meter_id,
lastEvent: getFormattedDate(data?.events[i]?.doc_fields['#time_raised_last'], '')
};
// Create popup template
const popupTemplate = {
title: "{eventCount} {watcherType} {plural}",
content:
"<ul><li><b>Address:</b> {address}</li>" +
"<li><b>Account ID:</b> {account}</li>" +
"<li><b>Meter ID:</b> {meterId}</li>" +
"<li><b>Last Event:</b> {lastEvent}</li>" +
"<li><a href='https://maps.google.com/maps?q=&layer=c&cbll={lat},{long}&cbp='>Google Street View</a></li></ul>"
};
// Add the multiPoints to a new graphic
const graphic = new Graphic({
geometry: point,
symbol: symbol,
attributes: attributes,
popupTemplate: popupTemplate
});
setGraphic(graphic);
props.view.graphics.add(graphic);
}
}).catch((err) => console.error(err));
return function cleanup() {
props.view.graphics.remove(graphic);
};
}, []);
return null;
}
export default PointsLayer
An image to visualize what I am working on:
Try adding the graphic state to the useEffect dependency array [graphic]
useEffect(function, [graphic]) or useEffect(function, [props]) but props may cause more re-renders than you may want
A more complete example would look like this.
const [graphic, setGraphic] = useState(null);
useEffect(() => {
loadModules(['esri/Graphic']).then(([Graphic]) => {
// Parse out the Lat-Long from each Event and the doc_count
for (let i = 0; i < data?.events.length; i++) {
const point = {
type: "point", // autocasts as new Point
longitude: data?.events[i]?.location.split(",")[1],
latitude: data?.events[i]?.location.split(",")[0]
};
// Create a symbol for rendering the graphic
const symbol = {
type: "simple-marker", // autocasts as new SimpleMarkerSymbol()
style: "circle", color: color, // Color Selected on popup
size: "12px", outline: {
color: [255, 255, 255], // White
width: 1.5
}
};
// Create attributes for popup
const attributes = {
watcherType: humanize(data?.events[i]?.doc_fields?.watcher_type),
eventCount: data?.events[i]?.doc_count,
plural: pluralize(data?.events[i]?.doc_count, 'Event'),
deviceName: data?.events[i]?.key,
lat: data?.events[i]?.location.split(",")[0],
long: data?.events[i]?.location.split(",")[1],
account: data?.events[i]?.doc_fields?.account_id,
address: data?.events[i]?.doc_fields['#service_address'],
meterId: data?.events[i]?.doc_fields?.meter_id,
lastEvent: getFormattedDate(data?.events[i]?.doc_fields['#time_raised_last'], '')
};
// Create popup template
const popupTemplate = {
title: "{eventCount} {watcherType} {plural}",
content:
"<ul><li><b>Address:</b> {address}</li>" +
"<li><b>Account ID:</b> {account}</li>" +
"<li><b>Meter ID:</b> {meterId}</li>" +
"<li><b>Last Event:</b> {lastEvent}</li>" +
"<li><a href='https://maps.google.com/maps?q=&layer=c&cbll={lat},{long}&cbp='>Google Street View</a></li></ul>"
};
// Add the multiPoints to a new graphic
const graphic = new Graphic({
geometry: point,
symbol: symbol,
attributes: attributes,
popupTemplate: popupTemplate
});
setGraphic(graphic);
props.view.graphics.add(graphic);
}
}).catch((err) => console.error(err));
return function cleanup() {
props.view.graphics.remove(graphic);
};
}, [graphic]);
or using props,
const [graphic, setGraphic] = useState(null);
useEffect(() => {
loadModules(['esri/Graphic']).then(([Graphic]) => {
// Parse out the Lat-Long from each Event and the doc_count
for (let i = 0; i < data?.events.length; i++) {
const point = {
type: "point", // autocasts as new Point
longitude: data?.events[i]?.location.split(",")[1],
latitude: data?.events[i]?.location.split(",")[0]
};
// Create a symbol for rendering the graphic
const symbol = {
type: "simple-marker", // autocasts as new SimpleMarkerSymbol()
style: "circle", color: color, // Color Selected on popup
size: "12px", outline: {
color: [255, 255, 255], // White
width: 1.5
}
};
// Create attributes for popup
const attributes = {
watcherType: humanize(data?.events[i]?.doc_fields?.watcher_type),
eventCount: data?.events[i]?.doc_count,
plural: pluralize(data?.events[i]?.doc_count, 'Event'),
deviceName: data?.events[i]?.key,
lat: data?.events[i]?.location.split(",")[0],
long: data?.events[i]?.location.split(",")[1],
account: data?.events[i]?.doc_fields?.account_id,
address: data?.events[i]?.doc_fields['#service_address'],
meterId: data?.events[i]?.doc_fields?.meter_id,
lastEvent: getFormattedDate(data?.events[i]?.doc_fields['#time_raised_last'], '')
};
// Create popup template
const popupTemplate = {
title: "{eventCount} {watcherType} {plural}",
content:
"<ul><li><b>Address:</b> {address}</li>" +
"<li><b>Account ID:</b> {account}</li>" +
"<li><b>Meter ID:</b> {meterId}</li>" +
"<li><b>Last Event:</b> {lastEvent}</li>" +
"<li><a href='https://maps.google.com/maps?q=&layer=c&cbll={lat},{long}&cbp='>Google Street View</a></li></ul>"
};
// Add the multiPoints to a new graphic
const graphic = new Graphic({
geometry: point,
symbol: symbol,
attributes: attributes,
popupTemplate: popupTemplate
});
setGraphic(graphic);
props.view.graphics.add(graphic);
}
}).catch((err) => console.error(err));
return function cleanup() {
props.view.graphics.remove(graphic);
};
}, [props]);
I would think just using the graphic state would be closer to what you are looking for. Leaving the dependency array empty causes the useEffect hook to only fire on the initial component mount.
Adding the graphic state to the array tells the useEffect to watch for changes in the graphic state, and if it changes, refire the useEffect again
This post may be helpful Hooks and Dependency Arrays

How can I delete an item inside a nested array with Hooks?

I am trying to remove a single item from state inside a nested array, but i am really struggling to understand how.
My data looks as follows, and I'm trying to remove one of the 'variants' objects on click.
const MYDATA = {
id: '0001',
title: 'A good title',
items: [
{
itemid: 0,
title: 'Cheddar',
variants: [
{ id: '062518', grams: 200, price: 3.00},
{ id: '071928', grams: 400, price: 5.50},
]
},
{
itemid: 1,
title: 'Edam',
variants: [
{ id: '183038', grams: 220, price: 2.50},
{ id: '194846', grams: 460, price: 4.99},
]
},
{
itemid: 2,
title: 'Red Leicester',
variants: [
{ id: '293834', grams: 420, price: 4.25},
{ id: '293837', grams: 660, price: 5.99},
]
}
]
}
Against each variant is a button which calls a remove function, which (should) remove the deleted item and update the state. However, this is not happening and I'm not sure what I am doing wrong.
const [myCheeses, setMyCheeses] = useState(MYDATA);
const removeMyCheese = (variantID, itemindx) => {
console.log(variantID);
setMyCheeses((prev) => {
const items = myCheeses.items[itemindx].variants.filter(
(variant) => variant.id !== variantID
);
console.log(items, itemindx);
return {
...myCheeses.items[itemindx].variants,
items
};
});
};
An example of the issue I'm facing can be seen here
https://codesandbox.io/s/funny-dan-c84cr?file=/src/App.js
Any help would be truly appreciated.
The issue is that, setMyCheeses function not returning the previous state including your change(removal)
Try one of these functions;
1st way
const removeMyCheese = (variantID, itemindx) => {
setMyCheeses((prev) => {
const items = myCheeses.items[itemindx].variants.filter(
(variant) => variant.id !== variantID
);
const newState = prev;
newState.items[itemindx].variants = items;
return {...newState};
});
};
https://codesandbox.io/s/bold-worker-b12x1?file=/src/App.js
2nd way
const removeMyCheese = (variantID, itemindx) => {
setMyCheeses((prev) => {
const items = myCheeses.items.map((item, index) => {
if (itemindx === index) {
return {
...item,
variants: item.variants.filter(
(variant) => variant.id !== variantID
)
};
} else {
return item;
}
});
return { ...prev, items: items };
});
};
https://codesandbox.io/s/sharp-forest-qhhwd
try this function, it's work for me :
const removeMyCheese = (variantID, itemindx) => {
//console.log(variantID);
const newMyCheeses = myCheeses;
const newItems = newMyCheeses.items.map((item) => {
return {
...item,
variants: item.variants.filter((variant) => variant.id !== variantID)
};
});
setMyCheeses({ ...newMyCheeses, items: newItems });
};
https://codesandbox.io/s/jolly-greider-fck6p?file=/src/App.js
Or, you can do somthing like this if you don't like to use the map function :
const removeMyCheese = (variantID, itemindx) => {
//console.log(variantID);
const newMyCheeses = myCheeses;
const newVariants = newMyCheeses.items[itemindx].variants.filter(
(variant) => variant.id !== variantID
);
newMyCheeses.items[itemindx].variants = newVariants;
setMyCheeses({ ...newMyCheeses });
};

tinyMCE React loosing state value

I'm using the tinyMCE editor in my React project. I need a custom button based on number of additional users. If it has 3 additional users, I add 3 additional buttons in my dropdown.
import { Editor } from '#tinymce/tinymce-react';
...
const [ totalAdditionalUsers, setTotalAdditionalUsers] = useState(0);
// I get this data from NodeJS backend and set the value inside my useEffect
// I'll simplify the code here
useEffect(() => {
setTotalAdditionalUsers(myVariable); // The value here is 3, for example
});
console.log(totalAdditionalUsers); // it shows 3
return (
<>
<Editor
apiKey={TINYMCEKEY}
value={editorContent}
init={{
height: 600,
menubar: false,
branding: false,
plugins: [
"print"
],
setup: function (editor) {
editor.ui.registry.addMenuButton('addAllSignatures', {
text: "Users Signature",
fetch: function (callback) {
var items = [
{
type: 'menuitem',
text: 'Primary User Signature',
onAction: function () {
editor.insertContent(' <strong>#userSignature#</strong> ');
}
}, {
type: 'menuitem',
text: 'Primary User Signature Date',
onAction: function () {
editor.insertContent(' <strong>#userSignatureDate#</strong> ');
}
}
];
console.log(totalAdditionalUsers); // It is showing 0. Why??
for(let i=1; i<=totalAdditionalUsers; i++) {
let s = 'th';
if(i === 1) s = 'nd';
else if(i === 2) s = 'th';
const objSign = {
type: 'menuitem',
text: `${(i+1)}${s}User Signature`,
onAction: function () {
editor.insertContent(` <strong>#addUser${i}#</strong> `);
}
};
const objDate = {
type: 'menuitem',
text: `${(i+1)}${s}User Signature Date`,
onAction: function () {
editor.insertContent(` <strong>#addUser${i}SignatureDate#</strong> `);
}
};
items.push(objSign);
items.push(objDate);
}
callback(items);
}
})
},
toolbar1: "print | addAllSignatures"
}}
onEditorChange={handleEditorChange}
/>
</>
);
My issue, it that inside the TinyMCE editor, the totalAdditionalUsers is always 0. Looks like it is not updating.
Am I setting in wrong?
Thanks

Resources