Functional Component Not Re-rendering after data is updated - reactjs

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

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.

why bad handling chartjs in a react component when render that component

I am working on a digital currency personal project.
My problem is that when I click on any digital currency to show the details of that coin,
The new page must be refreshed to display the chart correctly, or use a tag,
but I use the Link (react-router-dom),
And before refreshing the page, my chart is shown as below.
(https://ibb.co/NFgNMmX)
And after refreshing the page, my chart is shown as below.
(https://ibb.co/DzWRXRY)
and this is my code
const CoinChart = () => {
let id = useParams();
// state
const [chartDay, setChartDay] = useState({
value: "1",
text: "1d"
})
// redux
const dispatch = useDispatch<any>();
const detail = useSelector((state: State) => state.coin_detail.chart);
useEffect(() => {
dispatch(coinChartFetchRequestFunc(id.coin_id, chartDay.value));
}, [chartDay]);
// chart config
const labels = detail.chart?.prices.map((item) => {
if (chartDay.value == "max") {
return (
new Date(item[0]).toLocaleDateString()
)
}
else {
return (
new Date(item[0]).getDate() +
"." +
new Date(item[0]).toDateString().split(/[0-9]/)[0].split(" ")[1] +
" " +
new Date(item[0]).getHours() +
":" +
new Date(item[0]).getMinutes()
);
}
});
const Data = {
labels,
datasets: [
{
fill: true,
drawActiveElementsOnTop: false,
data: detail.chart?.prices.map((item) => {
return item[1];
}),
label: "price(usd)",
borderColor: "#3861fb",
backgroundColor: "#3861fb10",
pointBorderWidth: 0,
borderWidth: 2.5,
},
],
};
return (
//some code
<Line data={Data} />
);
};
Is this problem solvable?
Also, I used translate in some part, and if there are any problems in the above texts, excuse me!

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:

Using OpenLayers in React

I am student who is creating a big project trying to display data and information on maps using Openlayers and React. At this point I have created a basic two page project where the map is the central piece of the application. For every page I have one main Component that contains its Sidebar, the Viewer Component that contains the map itself and some additional Components specific for that page.
The viewer Class is a special Component that I wrote for every page. This Component handles all the user interactions with the page. It sends and receives information from the main Component as well. I have chosen to create a seperate Viewer Class for each page beceause of the diffrent ways each page works. They might load in different things or handle interactions completely different. If I had one Viewer Class that can be used by every page this would become to large and have to many if statements to check what code to run when handling with each separate page.
Here is an example of the Homepage Viewer. In the constructor I create the map and add some references for a popup window that shows up when the user clicks on one of the features on the map. This popup window shows some basic information about the feature and let's the user add it to his list of features.
In ComponentDidMount I first make the Map class add the
boundrieslayer and give an function to call when one of the feautres
of this layer has been clicked on. I also create the overlay window
for the popup to the map. This will be shown when a feature has been
clicked.
resetMapLayers is a function that has will be called on each Render.
Here I make the map check if what background Tilelayer to use and if
it should show the top layer or not.
featureSelected() is the function that handles a click event of a
feature of the top layer. This will create a popup with the basic
information of the feature.
closePopup will be called when the popup has been closed. This will
make the map deselct the clicked on feature. Otherwise it would stay
selected when the popup has been closed. It will also remove the
Overlay(popup) from the screen.
addFeature() is the function that will be called when a user chooses
to add this feature to his list of features. This can be done by the
user in the popup window of that feature
.
import React, { Component } from "react";
import { connect } from "react-redux";
import Map from "../Map/Map";
import "ol/ol.css";
import styles from "./Viewer.module.scss";
import Overlay from "ol/Overlay";
import Button from "../UI/Button/Button";
class MainViewer extends Component {
constructor(props) {
super(props);
Map.createNewMap();
this.popup = React.createRef();
this.popupContent = React.createRef();
}
componentDidMount() {
Map.addBoundriesLayer(this.featureSelected);
Map.map.setTarget("map");
let container = this.popup.current;
let overlay = new Overlay({
element: container,
autoPan: true,
autoPanAnimation: {
duration: 250,
},
});
this.overlay = overlay;
Map.map.addOverlay(overlay);
}
resetMapLayers() {
Map.setBackgroundTileLayer(this.props.type);
Map.togglePlotBoundriesLayers(this.props.plotBoundriesState);
}
featureSelected = (event, select) => {
if (event.selected[0]) {
this.selectedFeature = event.selected[0];
let selectedFeature = {
id: event.selected[0].id_,
gewasgroepnaam: event.selected[0].getProperties().GEWASGROEP,
gewasnaam: event.selected[0].getProperties().LBLHFDTLT,
oppervlak: (event.selected[0].getProperties().OPPERVL / 10000).toFixed(
2
),
coords: event.selected[0].getProperties().geometry.extent_,
};
let content = this.popupContent.current;
content.innerHTML =
"<p><strong>Name: </strong>" +
selectedFeature.name +
"</p>" +
"<p><strong>Exact Name: </strong>" +
selectedFeature.exactName +
"</p>" +
"<p><strong>Area: </strong>" +
selectedFeature.area +
" ha</p>";
this.overlay.setPosition(event.mapBrowserEvent.coordinate);
}
};
closePopup() {
this.overlay.setPosition(undefined);
Map.clearSelect();
return false;
}
addFeature() {
this.overlay.setPosition(undefined);
Map.clearSelect();
this.props.featureAddedHandler(this.selectedFeature);
}
render() {
this.resetMapLayers();
return (
<div>
<div id="map" className={styles.Map}></div>
<div ref={this.popup} className={styles.OlPopup}>
<div className={styles.OlPopupButtonsDiv}>
<Button
btnType="Danger"
className={[styles.PopupButton, styles.ClosePopupButton].join(
" "
)}
clicked={() => this.closePopup()}
>
Annuleer
</Button>
<Button
btnType="Success"
className={[styles.PopupButton, styles.AddPopupButton].join(" ")}
clicked={() => this.addFeature()}
>
Voeg Toe
</Button>
</div>
<div ref={this.popupContent}></div>
</div>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
type: state.mapDetails.type,
plotBoundriesState: state.mapDetails.state,
};
};
export default connect(mapStateToProps)(MainViewer);
The Map class contains the actual map. It handles with all the specific map issues. It allows to add layers to the map, creating the initial map, adding map interactions,...
In the constructor I call the methods to initaly create a basic map
with a TileLayer background.
The createMap and createNewMap methods let's me create the map object
itself.
createBackgroundLayerGroups() creates the Tilelayer that is the
background layer. It can be either OSM or Bing Maps. The visibility
property let's me make handle which one to show.
clearAllBoundriesLayers() deletes all the boundries layers that are
able to be put on top of the Tilelayer. It deletes every layer and
clears the select of the interactions that has been added to these
layers. I do this so when I change page the layers will be deleted.
addBoundriesLayer let's me set and add a boundries layer. This is the
Vector layer that will be put on top of the TileLayer.
addUsersPlotBoundriesLayer does the same as "addBoundriesLayer" but does it for all the layers that the user has. This function will be called in another page that only shows the features of the user.
setExtentOfMapByUserFeaters let's me set the extent of the map by
either a given extent or by the features of the user.
setInteractionForPlotBoundriesLayer adds the interaction for the
PlotBoundriesLayer.
setHoverInteractionForUserPlotBoundries adds the hover interaction
for the plotUserBoundriesLayer.
clearSelect clears all the selected features. So they won't be
highlighted no more.
setBackgroundTileLayer let's me show a specific background TileLayer.
togglePlotBoundriesLayers let's me hide or show the Vector layer that
is then been shown.
import Map from "ol/Map";
import TileWMS from "ol/source/TileWMS";
import TileLayer from "ol/layer/Tile";
import View from "ol/View";
import OSM from "ol/source/OSM";
import BingMaps from "ol/source/BingMaps";
import VectorSource from "ol/source/Vector";
import { bbox as bboxStrategy } from "ol/loadingstrategy";
import GeoJSON from "ol/format/GeoJSON";
import { Vector, Group, Tile } from "ol/layer";
import Select from "ol/interaction/Select";
import { Feature } from "ol";
import { Polygon } from "ol/geom";
import { Fill, Stroke, Style } from "ol/style";
class OlMap {
constructor() {
this.createNewMap();
this.createBackgroundLayerGroups();
}
createNewMap() {
this.map = this.createMap();
}
createMap() {
return new Map({
target: null,
layers: [],
view: new View({
center: [594668.0262129545, 6602083.305674396],
maxZoom: 19,
zoom: 14,
}),
});
}
createBackgroundLayerGroups() {
this.layersOSM = new Group({
layers: [
new Tile({
source: new OSM(),
}),
new Tile({
source: new BingMaps({
imagerySet: "Aerial",
key: process.env.REACT_APP_BING_MAPS,
}),
visible: false,
}),
],
});
}
clearAllBoundriesLayers() {
this.map.getLayers().forEach((layer) => {
if (
layer.get("name") === "plotBoundriesLayer" ||
layer.get("name") === "plotUserBoundriesLayer"
) {
layer.getSource().clear();
this.map.removeLayer(layer);
}
});
if (this.select) {
this.select.getFeatures().clear();
}
}
addBoundriesLayer(featureSelected) {
this.clearAllBoundriesLayers();
let vectorSource = new VectorSource({
format: new GeoJSON(),
minScale: 15000000,
loader: function (extent, resolution, projection) {
/*
Link for the DLV
let url = process.env.REACT_APP_MAP_API +
extent.join(",") +
",EPSG:3857";
*/ let url = process.env.REACT_APP_MAP_API +
extent.join(",") +
",EPSG:3857";
// */
let xhr = new XMLHttpRequest();
xhr.open("GET", url);
let onError = function () {
vectorSource.removeLoadedExtent(extent);
};
xhr.onerror = onError;
xhr.onload = function () {
if (xhr.status === 200) {
let features = vectorSource
.getFormat()
.readFeatures(xhr.responseText);
features.forEach(function (feature) {
//ID for the DLV
//feature.setId(feature.get("OBJ_ID"));
feature.setId(feature.get("OIDN"));
});
vectorSource.addFeatures(features);
} else {
onError();
}
};
xhr.send();
},
strategy: bboxStrategy,
});
let vector = new Vector({
//minZoom: 13,
source: vectorSource,
});
this.setInteractionForPlotBoundriesLayer(vector, featureSelected);
vector.set("name", "plotBoundriesLayer");
this.map.addLayer(vector);
}
addUsersPlotBoundriesLayer(featureSelected, featureHovered, newFeatures) {
this.clearAllBoundriesLayers();
if (newFeatures.length > 0) {
let vectorSource = new VectorSource({
format: new GeoJSON(),
minScale: 15000000,
strategy: bboxStrategy,
});
newFeatures.forEach((newFeature) => {
let feature = new Feature({
geometry: new Polygon([newFeature.geometry]),
});
feature.setId(newFeature.plotId);
vectorSource.addFeature(feature);
});
let vector = new Vector({
//minZoom: 13,
source: vectorSource,
});
this.setInteractionForPlotBoundriesLayer(vector, featureSelected);
this.setHoverInteractionForUserPlotBoundries(vector, featureHovered);
vector.set("name", "plotUserBoundriesLayer");
this.plotsExtent = vectorSource.getExtent();
this.map.addLayer(vector);
}
}
setExtentOfMapByUserFeaters(extent) {
if (extent === undefined) {
if (this.plotsExtent !== undefined && this.plotsExtent[0] !== Infinity) {
this.map.getView().fit(this.plotsExtent);
}
} else {
this.map.getView().fit(extent);
}
}
setInteractionForPlotBoundriesLayer(layer, featureSelected) {
this.select = new Select({
layers: [layer],
});
this.select.on("select", (event) => featureSelected(event, this.select));
this.map.addInteraction(this.select);
}
setHoverInteractionForUserPlotBoundries(layer, featureHovered) {
this.hoveredFeature = null;
let defaultStyle = new Style({
stroke: new Stroke({
width: 2,
color: "#9c1616",
}),
fill: new Fill({ color: "#c04e4e" }),
});
let hoveredStyle = new Style({
stroke: new Stroke({
width: 2,
color: "#9c1616",
}),
fill: new Fill({ color: "#9c1616" }),
});
this.map.on("pointermove", (e) => {
layer
.getSource()
.getFeatures()
.forEach((feature) => {
feature.setStyle(defaultStyle);
});
let newFeature = null;
this.map.forEachFeatureAtPixel(e.pixel, (f) => {
newFeature = f;
newFeature.setStyle(hoveredStyle);
return true;
});
if (newFeature) {
if (
this.hoveredFeature === null ||
this.hoveredFeature !== newFeature
) {
this.hoveredFeature = newFeature;
featureHovered(this.hoveredFeature.id_);
}
} else {
if (this.hoveredFeature !== null) {
this.hoveredFeature = null;
featureHovered(null);
}
}
});
}
hoveredSideBarFeatureHandler(hoveredFeatureId) {
let defaultStyle = new Style({
stroke: new Stroke({
width: 2,
color: "#9c1616",
}),
fill: new Fill({ color: "#c04e4e" }),
});
let hoveredStyle = new Style({
stroke: new Stroke({
width: 2,
color: "#9c1616",
}),
fill: new Fill({ color: "#9c1616" }),
});
this.map.getLayers().forEach((layer) => {
if (layer.get("name") === "plotUserBoundriesLayer") {
layer
.getSource()
.getFeatures()
.forEach((feature) => {
if (feature.id_ === hoveredFeatureId) {
feature.setStyle(hoveredStyle);
} else {
feature.setStyle(defaultStyle);
}
});
}
});
}
clearSelect() {
this.select.getFeatures().clear();
}
setBackgroundTileLayer(type) {
if (this.backgroundTileType === null) {
this.backgroundTileType = "OPENSTREETMAP";
}
if (this.map.getLayers().getArray().length === 0) {
this.map.setLayerGroup(this.layersOSM);
} else {
if (this.backgroundTileType !== type) {
this.backgroundTileType = type;
console.log(this.map.getLayers());
this.map.getLayers().getArray()[0].setVisible(false);
this.map.getLayers().getArray()[1].setVisible(false);
if (type === "OPENSTREETMAP") {
this.map.getLayers().getArray()[0].setVisible(true);
} else if (type === "BING MAPS") {
this.map.getLayers().getArray()[1].setVisible(true);
}
}
}
}
togglePlotBoundriesLayers(state) {
if (this.plotBoundriesState === null) {
this.plotBoundriesState = true;
}
if (this.plotBoundriesState !== state) {
this.plotBoundriesState = state;
this.map.getLayers().forEach((layer) => {
if (layer.get("name") === "plotBoundriesLayer") {
layer.setVisible(state);
}
if (layer.get("name") === "plotUserBoundriesLayer") {
console.log(state);
layer.setVisible(state);
}
});
}
}
addTileLayer(url) {
const wmsLayer = new TileLayer({
source: new TileWMS({
url,
params: {
TILED: true,
},
crossOrigin: "Anonymous",
}),
});
this.map.addLayer(wmsLayer);
}
}
export default new OlMap();
At this point I am wondering if I am doing things well or what can be done different or better to optimize my code. This will help me in the long run not getting stuck with bade code I created at the beginning of the project.
Many thanks in advance!

display openlayers infowindow in React

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>';
}
}

Resources