@eeacms/volto-bise-policy 1.3.11 → 1.3.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/package.json +2 -1
  3. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyCard.jsx +404 -0
  4. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyCard.test.jsx +366 -0
  5. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyExplorerView.jsx +192 -91
  6. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyExplorerView.test.jsx +205 -58
  7. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMap.jsx +329 -178
  8. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMap.test.jsx +243 -123
  9. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMapLazy.jsx +61 -0
  10. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMapLazy.test.jsx +89 -0
  11. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyResults.jsx +128 -0
  12. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyResults.test.jsx +160 -0
  13. package/src/components/manage/Blocks/CaseStudyExplorer/FacetPopover.jsx +180 -0
  14. package/src/components/manage/Blocks/CaseStudyExplorer/FacetPopover.test.jsx +125 -0
  15. package/src/components/manage/Blocks/CaseStudyExplorer/FilterPanel.jsx +171 -0
  16. package/src/components/manage/Blocks/CaseStudyExplorer/FilterPanel.test.jsx +181 -0
  17. package/src/components/manage/Blocks/CaseStudyExplorer/Pagination.jsx +81 -0
  18. package/src/components/manage/Blocks/CaseStudyExplorer/filters.js +350 -0
  19. package/src/components/manage/Blocks/CaseStudyExplorer/filters.test.js +399 -0
  20. package/src/components/manage/Blocks/CaseStudyExplorer/hooks.js +28 -4
  21. package/src/components/manage/Blocks/CaseStudyExplorer/hooks.test.js +73 -0
  22. package/src/components/manage/Blocks/CaseStudyExplorer/messages.js +138 -0
  23. package/src/components/manage/Blocks/CaseStudyExplorer/styles.less +1157 -317
  24. package/src/components/manage/Blocks/CaseStudyExplorer/testUtils.js +44 -46
  25. package/src/components/manage/Blocks/CaseStudyExplorer/utils.js +68 -184
  26. package/src/components/manage/Blocks/CaseStudyExplorer/utils.test.js +91 -158
  27. package/src/index.js +39 -0
  28. package/src/openlayers.js +51 -0
  29. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyFilters.jsx +0 -383
  30. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyFilters.test.jsx +0 -90
  31. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyListing.jsx +0 -342
  32. package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyListing.test.jsx +0 -196
  33. package/src/components/manage/Blocks/CaseStudyExplorer/FeatureDisplay.jsx +0 -24
  34. package/src/components/manage/Blocks/CaseStudyExplorer/FeatureDisplay.test.jsx +0 -49
  35. package/src/components/manage/Blocks/CaseStudyExplorer/FeatureInteraction.jsx +0 -100
  36. package/src/components/manage/Blocks/CaseStudyExplorer/FeatureInteraction.test.jsx +0 -189
  37. package/src/components/manage/Blocks/CaseStudyExplorer/InfoOverlay.jsx +0 -111
  38. package/src/components/manage/Blocks/CaseStudyExplorer/InfoOverlay.test.jsx +0 -159
  39. package/src/components/manage/Blocks/CaseStudyExplorer/styles.less_old +0 -201
@@ -1,5 +1,4 @@
1
1
  import React from 'react';
2
-
3
2
  import cx from 'classnames';
4
3
 
5
4
  import {
@@ -12,49 +11,77 @@ import {
12
11
  import { withOpenLayers } from '@eeacms/volto-openlayers-map';
13
12
  import { GISCO_OSM_ATTRIBUTION } from '@eeacms/volto-bise-policy/constants';
14
13
 
15
- import InfoOverlay from './InfoOverlay';
16
- import FeatureInteraction from './FeatureInteraction';
17
- import CaseStudyList from './CaseStudyListing';
14
+ import { useIntl } from 'react-intl';
18
15
 
16
+ import messages from './messages';
19
17
  import {
20
- centerAndResetMapZoom,
21
18
  CLUSTER_COLOR,
19
+ SELECTED_COLOR,
20
+ STATUS_COLORS,
21
+ centerAndResetMapZoom,
22
22
  getFeatures,
23
- getSelectInteraction,
24
- scrollToElement,
25
23
  } from './utils';
26
24
 
27
25
  const styleCache = {};
26
+
28
27
  const MapContextGateway = ({ setMap }) => {
29
28
  const { map } = useMapContext();
29
+
30
30
  React.useEffect(() => {
31
31
  setMap(map);
32
32
  }, [map, setMap]);
33
+
33
34
  return null;
34
35
  };
35
36
 
36
- function CaseStudyMap(props) {
37
- const {
38
- items,
39
- activeItems,
40
- hideFilters,
41
- selectedCase,
42
- onSelectedCase,
43
- searchInput,
44
- map,
45
- setMap,
46
- ol,
47
- } = props;
48
- const features = getFeatures({ cases: items, ol });
37
+ const LEGEND = [
38
+ { status: 'planned', label: messages.statusPlanned },
39
+ { status: 'ongoing', label: messages.statusOngoing },
40
+ { status: 'completed', label: messages.statusCompleted },
41
+ ];
42
+
43
+ function getFeaturePath(feature) {
44
+ return feature?.get?.('path');
45
+ }
46
+
47
+ /**
48
+ * Case study map. Pins are coloured by current status and clustered when
49
+ * they overlap; clicking a pin selects its case study (the pin turns dark
50
+ * green, a popup shows its title and the results list opens the page holding
51
+ * its card), clicking empty map space clears the selection.
52
+ */
53
+ function CaseStudyMap({
54
+ items = [],
55
+ selectedPath,
56
+ onSelect,
57
+ setMap,
58
+ map,
59
+ focusRequest,
60
+ resetRequest,
61
+ ol,
62
+ }) {
63
+ const intl = useIntl();
64
+ const features = React.useMemo(
65
+ () => getFeatures({ cases: items, ol }),
66
+ [items, ol],
67
+ );
49
68
  const [resetMapButtonClass, setResetMapButtonClass] =
50
69
  React.useState('inactive');
70
+ const [mapReady, setMapReady] = React.useState(false);
71
+ const containerRef = React.useRef(null);
72
+ const popupRef = React.useRef(null);
73
+
74
+ const selectedItem = React.useMemo(
75
+ () => items.find((item) => item.properties?.path === selectedPath) || null,
76
+ [items, selectedPath],
77
+ );
78
+ const popupTitle = selectedItem?.properties?.title || null;
51
79
 
52
80
  const [tileWMSSources] = React.useState([
53
81
  new ol.source.TileWMS({
54
82
  url: 'https://gisco-services.ec.europa.eu/maps/service',
55
83
  attributions: GISCO_OSM_ATTRIBUTION,
56
84
  params: {
57
- // LAYERS: 'OSMBlossomComposite', OSMCartoComposite, OSMPositronComposite
58
85
  LAYERS: 'OSMPositronComposite',
59
86
  TILED: true,
60
87
  },
@@ -62,12 +89,7 @@ function CaseStudyMap(props) {
62
89
  transition: 0,
63
90
  }),
64
91
  ]);
65
- const [pointsSource] = React.useState(
66
- new ol.source.Vector({
67
- features,
68
- }),
69
- );
70
-
92
+ const [pointsSource] = React.useState(new ol.source.Vector());
71
93
  const [clusterSource] = React.useState(
72
94
  new ol.source.Cluster({
73
95
  distance: 19,
@@ -75,110 +97,220 @@ function CaseStudyMap(props) {
75
97
  }),
76
98
  );
77
99
 
78
- // `ol` is a fresh object literal on every render (see withOpenLayers); keep
79
- // the latest reference in a ref so effects can depend only on values that
80
- // actually change. Refreshing the points source regenerates the cluster
81
- // features, which drops the currently selected feature from the rendered set.
100
+ // `ol` is a fresh object literal on every render (see withOpenLayers), so
101
+ // effects must not depend on it.
82
102
  const olRef = React.useRef(ol);
83
103
  olRef.current = ol;
84
104
 
85
105
  React.useEffect(() => {
86
- if (activeItems) {
87
- pointsSource.clear();
88
- pointsSource.addFeatures(
89
- getFeatures({ cases: activeItems, ol: olRef.current }),
90
- );
91
- }
92
- }, [activeItems, pointsSource]);
106
+ pointsSource.clear();
107
+ pointsSource.addFeatures(features);
108
+ }, [features, pointsSource]);
109
+
110
+ // The add-on's `Layer.Vector` applies the `style` prop only when it creates
111
+ // the layer: later prop changes go through `layer.set(...)`, which cannot
112
+ // replace an OpenLayers style. So the style function has to stay stable and
113
+ // read the current selection from a ref, and the pins have to be redrawn by
114
+ // hand whenever that selection changes.
115
+ const selectedPathRef = React.useRef(selectedPath);
116
+ selectedPathRef.current = selectedPath;
117
+
118
+ const clusterStyle = React.useMemo(
119
+ () => clusterStyleFor({ ol, selectedPathRef }),
120
+ [ol],
121
+ );
122
+
123
+ React.useEffect(() => {
124
+ // Dispatches a change on the source, which makes the cluster source
125
+ // re-cluster and the layer paint again — with the new selection colour.
126
+ pointsSource.changed();
127
+ }, [selectedPath, pointsSource]);
128
+
129
+ // Keep the title popup over its pin: OpenLayers positions features, not DOM
130
+ // nodes, so the anchor is moved on every frame the map renders (pan, zoom,
131
+ // resize) and hidden while the pin is outside the viewport.
132
+ React.useEffect(() => {
133
+ if (!map || !selectedItem) return undefined;
134
+
135
+ const anchor = popupRef.current;
136
+ const coordinates = selectedItem.geometry?.coordinates;
137
+ if (!anchor || !coordinates) return undefined;
138
+
139
+ const position = olRef.current.proj.fromLonLat(coordinates);
140
+ let lastX = null;
141
+ let lastY = null;
142
+
143
+ const place = () => {
144
+ const pixel = map.getPixelFromCoordinate(position);
145
+ if (!pixel) return;
146
+
147
+ const x = Math.round(pixel[0]);
148
+ const y = Math.round(pixel[1]);
149
+ const size = map.getSize?.();
150
+ const inView =
151
+ !size || (x >= 0 && y >= 0 && x <= size[0] && y <= size[1]);
152
+
153
+ if (x === lastX && y === lastY) return;
154
+ lastX = x;
155
+ lastY = y;
156
+
157
+ anchor.style.transform = `translate(${x}px, ${y}px)`;
158
+ anchor.style.visibility = inView ? 'visible' : 'hidden';
159
+ };
160
+
161
+ place();
162
+ map.on('postrender', place);
163
+
164
+ return () => {
165
+ map.un('postrender', place);
166
+ };
167
+ }, [map, selectedItem]);
93
168
 
94
169
  React.useEffect(() => {
95
- if (!map) return;
96
-
97
- const moveendListener = () => {
98
- const mapZoom = Math.round(map.getView().getZoom() * 10) / 10;
99
- const mapCenter = map.getView().getCenter();
100
- const selectInteraction = getSelectInteraction(map);
101
-
102
- if (selectInteraction) {
103
- if (selectedCase) {
104
- const coords = selectedCase.geometry.flatCoordinates;
105
- const pixel = map.getPixelFromCoordinate(coords);
106
- const selectedFeature = map.getFeaturesAtPixel(pixel)[0];
107
-
108
- selectInteraction.getFeatures().clear();
109
- if (selectedFeature) {
110
- selectInteraction.getFeatures().push(selectedFeature);
111
- }
112
- } else {
113
- selectInteraction.getFeatures().clear();
114
- }
170
+ if (!map) return undefined;
171
+
172
+ const onMapClick = (event) => {
173
+ const feature = map.forEachFeatureAtPixel(event.pixel, (hit) => hit);
174
+
175
+ if (!feature) {
176
+ onSelect?.(null);
177
+ return;
115
178
  }
116
179
 
117
- if (
118
- mapZoom === 4 &&
119
- JSON.stringify(mapCenter) ===
120
- JSON.stringify(ol.proj.transform([10, 49], 'EPSG:4326', 'EPSG:3857'))
121
- ) {
122
- setResetMapButtonClass('inactive');
123
- } else {
124
- setResetMapButtonClass('active');
180
+ const members = feature.get('features') || [];
181
+ if (members.length === 1) {
182
+ onSelect?.(getFeaturePath(members[0]));
183
+ return;
125
184
  }
185
+
186
+ // A cluster: let the visitor zoom into it rather than guess a pin.
187
+ onSelect?.(null);
188
+ const extent = olRef.current.extent.createEmpty();
189
+ members.forEach((member) => {
190
+ olRef.current.extent.extend(extent, member.getGeometry().getExtent());
191
+ });
192
+ map.getView().fit(extent, { duration: 500, padding: [40, 40, 40, 40] });
193
+ };
194
+
195
+ const onPointerMove = (event) => {
196
+ const hit = map.hasFeatureAtPixel(map.getEventPixel(event.originalEvent));
197
+ map.getViewport().style.cursor = hit ? 'pointer' : '';
198
+ };
199
+
200
+ map.on('click', onMapClick);
201
+ map.on('pointermove', onPointerMove);
202
+
203
+ return () => {
204
+ map.un('click', onMapClick);
205
+ map.un('pointermove', onPointerMove);
126
206
  };
207
+ }, [map, onSelect]);
127
208
 
128
- map.on('moveend', moveendListener);
209
+ // The container is sized by CSS (a square that follows the column width),
210
+ // so OpenLayers has to be told whenever that box changes size. Without this
211
+ // the canvas keeps the height it measured at mount and leaves the bottom of
212
+ // the square blank.
213
+ React.useEffect(() => {
214
+ if (!map) return undefined;
215
+
216
+ const container = containerRef.current;
217
+ if (!container) return undefined;
218
+
219
+ const syncSize = () => map.updateSize();
220
+ syncSize();
221
+
222
+ if (typeof ResizeObserver === 'undefined') {
223
+ window.addEventListener('resize', syncSize);
224
+ return () => window.removeEventListener('resize', syncSize);
225
+ }
226
+
227
+ const observer = new ResizeObserver(syncSize);
228
+ observer.observe(container);
229
+ return () => observer.disconnect();
230
+ }, [map]);
231
+
232
+ // The map instance exists before its first frame is painted: keep the
233
+ // loader up until OpenLayers reports that everything (tiles included) has
234
+ // finished rendering.
235
+ React.useEffect(() => {
236
+ if (!map || mapReady) return undefined;
237
+
238
+ const markReady = () => setMapReady(true);
239
+ map.on('rendercomplete', markReady);
240
+ // Never leave the loader up forever: a failing tile source or a hidden
241
+ // tab should not hide the map.
242
+ const fallback = window.setTimeout(markReady, 10000);
243
+
244
+ return () => {
245
+ map.un('rendercomplete', markReady);
246
+ window.clearTimeout(fallback);
247
+ };
248
+ }, [map, mapReady]);
249
+
250
+ React.useEffect(() => {
251
+ if (!map) return undefined;
252
+
253
+ const onMoveEnd = () => {
254
+ const zoom = Math.round(map.getView().getZoom() * 10) / 10;
255
+ const center = map.getView().getCenter();
256
+ const atHome =
257
+ zoom === 4.5 &&
258
+ JSON.stringify(center) ===
259
+ JSON.stringify(
260
+ olRef.current.proj.transform([8, 55], 'EPSG:4326', 'EPSG:3857'),
261
+ );
262
+
263
+ setResetMapButtonClass(atHome ? 'inactive' : 'active');
264
+ };
265
+
266
+ map.on('moveend', onMoveEnd);
129
267
 
130
268
  return () => {
131
- map.un('moveend', moveendListener);
269
+ map.un('moveend', onMoveEnd);
132
270
  };
133
- }, [map, selectedCase, ol.proj, setResetMapButtonClass]);
271
+ }, [map]);
272
+
273
+ // "Show on map" on a card: fly to that case study without touching the
274
+ // current selection.
275
+ React.useEffect(() => {
276
+ if (!map || !focusRequest?.path) return;
277
+
278
+ const target = items.find(
279
+ (item) => item.properties?.path === focusRequest.path,
280
+ );
281
+ const coordinates = target?.geometry?.coordinates;
282
+ if (!coordinates) return;
283
+
284
+ map.getView().animate({
285
+ center: olRef.current.proj.fromLonLat(coordinates),
286
+ zoom: 8,
287
+ duration: 700,
288
+ });
289
+ // focusRequest.nonce changes on every request, even for the same case.
290
+ // eslint-disable-next-line react-hooks/exhaustive-deps
291
+ }, [focusRequest?.nonce]);
134
292
 
135
- const clusterStyle = React.useMemo(() => selectedClusterStyle({ ol }), [ol]);
293
+ // "Reset map" on an already selected card: go back to the European extent.
294
+ React.useEffect(() => {
295
+ if (!map || !resetRequest?.nonce) return;
136
296
 
137
- const MapWithSelection = React.useMemo(() => Map, []);
138
- // console.log('render');
297
+ centerAndResetMapZoom({ map, ol: olRef.current });
298
+ // resetRequest.nonce changes on every request.
299
+ // eslint-disable-next-line react-hooks/exhaustive-deps
300
+ }, [resetRequest?.nonce]);
139
301
 
140
- return features.length > 0 ? (
141
- <div id="ol-map-container" className="nrr-case-study-map-container">
142
- <MapWithSelection
302
+ return (
303
+ <div className="cse-map" id="cse-map-container" ref={containerRef}>
304
+ <Map
143
305
  view={{
144
- center: ol.proj.fromLonLat([10, 49]),
306
+ center: ol.proj.fromLonLat([8, 55]),
145
307
  showFullExtent: true,
146
- zoom: 4,
308
+ zoom: 4.5,
147
309
  }}
148
310
  pixelRatio={1}
149
311
  >
150
- <Controls
151
- attribution={true}
152
- attributionOptions={{ collapsible: false }}
153
- />
312
+ <Controls attribution attributionOptions={{ collapsible: false }} />
154
313
  <Layers>
155
- {hideFilters ? null : (
156
- <button
157
- className={cx(
158
- 'reset-map-button ui button secondary',
159
- String(resetMapButtonClass),
160
- )}
161
- onClick={() => {
162
- scrollToElement('search-input');
163
- onSelectedCase(null);
164
- centerAndResetMapZoom({ map, ol });
165
- getSelectInteraction(map)?.getFeatures().clear();
166
- }}
167
- >
168
- <span className="result-info-title">Reset map</span>
169
- <i className="icon ri-map-2-line"></i>
170
- </button>
171
- )}
172
- <InfoOverlay
173
- selectedFeature={selectedCase}
174
- onFeatureSelect={onSelectedCase}
175
- layerId={tileWMSSources[0]}
176
- hideFilters={hideFilters}
177
- />
178
- <FeatureInteraction
179
- onFeatureSelect={onSelectedCase}
180
- hideFilters={hideFilters}
181
- />
182
314
  <Layer.Tile source={tileWMSSources[0]} zIndex={0} />
183
315
  <Layer.Vector
184
316
  style={clusterStyle}
@@ -187,88 +319,107 @@ function CaseStudyMap(props) {
187
319
  />
188
320
  <MapContextGateway setMap={setMap} />
189
321
  </Layers>
190
- </MapWithSelection>
191
- {hideFilters ? null : (
192
- <>
193
- <div
194
- className="case-study-status-legend"
195
- aria-label="Case study status legend"
196
- >
197
- <span className="legend-title">Current status</span>
198
- {[
199
- ['planned', '#006bb8'],
200
- ['ongoing', '#ff9933'],
201
- ['completed', '#00a390'],
202
- ].map(([status, color]) => (
203
- <span className="legend-item" key={status}>
204
- <span
205
- className="legend-dot"
206
- style={{ backgroundColor: color }}
207
- />
208
- {status}
209
- </span>
210
- ))}
211
- </div>
212
- <CaseStudyList
213
- map={map}
214
- activeItems={activeItems}
215
- selectedCase={selectedCase}
216
- onSelectedCase={onSelectedCase}
217
- pointsSource={pointsSource}
218
- searchInput={searchInput}
219
- />
220
- </>
322
+ </Map>
323
+
324
+ {popupTitle && (
325
+ <div className="cse-map-popup-anchor" ref={popupRef}>
326
+ <div className="cse-map-popup">{popupTitle}</div>
327
+ </div>
221
328
  )}
329
+
330
+ {!mapReady && (
331
+ <div className="cse-map-loading" role="status" aria-live="polite">
332
+ <span className="cse-loading-spinner" aria-hidden="true" />
333
+ {intl.formatMessage(messages.loadingMap)}
334
+ </div>
335
+ )}
336
+
337
+ <button
338
+ type="button"
339
+ className={cx('cse-map-reset', String(resetMapButtonClass))}
340
+ onClick={() => {
341
+ onSelect?.(null);
342
+ centerAndResetMapZoom({ map, ol });
343
+ }}
344
+ >
345
+ <i className="icon ri-map-2-line" aria-hidden="true" />
346
+ {intl.formatMessage(messages.resetMap, {})}
347
+ </button>
348
+
349
+ <div
350
+ className="cse-map-legend"
351
+ aria-label={intl.formatMessage(messages.mapLegend)}
352
+ >
353
+ <span className="cse-map-legend-title">
354
+ {intl.formatMessage(messages.facetStatus)}
355
+ </span>
356
+ <ul className="cse-map-legend-items">
357
+ {LEGEND.map(({ status, label }) => (
358
+ <li key={status} className="cse-map-legend-item">
359
+ <span
360
+ className="cse-map-legend-dot"
361
+ style={{ backgroundColor: STATUS_COLORS[status] }}
362
+ aria-hidden="true"
363
+ />
364
+ {intl.formatMessage(label)}
365
+ </li>
366
+ ))}
367
+ </ul>
368
+ </div>
222
369
  </div>
223
- ) : null;
370
+ );
224
371
  }
225
372
 
226
- const selectedClusterStyle = ({ ol }) => {
227
- function _clusterStyle(feature) {
228
- const size = feature.get('features').length;
229
- let style = styleCache[size];
373
+ /**
374
+ * Cluster style: coloured ring per status for a single case study, a dark
375
+ * green disc for the selected one, a numbered bubble for a cluster.
376
+ */
377
+ const clusterStyleFor = ({ ol, selectedPathRef }) => {
378
+ const styleFor = (size) => {
379
+ if (styleCache[size]) return styleCache[size];
230
380
 
231
- if (!style) {
232
- style = new ol.style.Style({
233
- image: new ol.style.Circle({
234
- radius: 12 + Math.min(Math.floor(size / 3), 10),
235
- stroke: new ol.style.Stroke({
236
- color: '#fff',
237
- }),
238
- fill: new ol.style.Fill({
239
- color: CLUSTER_COLOR,
240
- }),
241
- }),
242
- text: new ol.style.Text({
243
- text: size.toString(),
244
- fill: new ol.style.Fill({
245
- color: '#fff',
246
- }),
247
- }),
248
- });
249
- styleCache[size] = style;
250
- }
381
+ styleCache[size] = new ol.style.Style({
382
+ image: new ol.style.Circle({
383
+ radius: 12 + Math.min(Math.floor(size / 3), 10),
384
+ stroke: new ol.style.Stroke({ color: '#FFFFFF', width: 2 }),
385
+ fill: new ol.style.Fill({ color: CLUSTER_COLOR }),
386
+ }),
387
+ text: new ol.style.Text({
388
+ text: size.toString(),
389
+ fill: new ol.style.Fill({ color: '#FFFFFF' }),
390
+ }),
391
+ });
392
+
393
+ return styleCache[size];
394
+ };
395
+
396
+ return (feature) => {
397
+ const members = feature.get('features') || [];
251
398
 
252
- if (size === 1) {
253
- const color = feature.values_.features[0].get('color');
399
+ if (members.length !== 1) return styleFor(members.length);
254
400
 
401
+ const member = members[0];
402
+ const selectedPath = selectedPathRef.current;
403
+
404
+ if (selectedPath && member.get('path') === selectedPath) {
255
405
  return new ol.style.Style({
406
+ zIndex: 10,
256
407
  image: new ol.style.Circle({
257
- radius: 6,
258
- fill: new ol.style.Fill({
259
- color: '#fff',
260
- }),
261
- stroke: new ol.style.Stroke({
262
- color: color,
263
- width: 6,
264
- }),
408
+ radius: 11,
409
+ fill: new ol.style.Fill({ color: SELECTED_COLOR }),
410
+ stroke: new ol.style.Stroke({ color: '#FFFFFF', width: 1 }),
265
411
  }),
266
412
  });
267
- } else {
268
- return style;
269
413
  }
270
- }
271
- return _clusterStyle;
414
+
415
+ return new ol.style.Style({
416
+ image: new ol.style.Circle({
417
+ radius: 7,
418
+ fill: new ol.style.Fill({ color: '#FFFFFF' }),
419
+ stroke: new ol.style.Stroke({ color: member.get('color'), width: 5 }),
420
+ }),
421
+ });
422
+ };
272
423
  };
273
424
 
274
425
  export default withOpenLayers(CaseStudyMap);