@eeacms/volto-bise-policy 1.2.32 → 1.2.34
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.
- package/CHANGELOG.md +34 -0
- package/package.json +3 -1
- package/src/components/Widgets/GeolocationWidget.jsx +143 -0
- package/src/components/Widgets/GeolocationWidgetMapContainer.jsx +131 -0
- package/src/components/Widgets/NRRWidgets.jsx +95 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyExplorerEdit.jsx +5 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyExplorerView.jsx +107 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyExplorerView.test.jsx +89 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyFilters.jsx +339 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyFilters.test.jsx +111 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyListing.jsx +330 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyListing.test.jsx +166 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMap.jsx +237 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMap.test.jsx +176 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/FeatureDisplay.jsx +41 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/FeatureDisplay.test.jsx +32 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/FeatureInteraction.jsx +98 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/FeatureInteraction.test.jsx +160 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/InfoOverlay.jsx +82 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/InfoOverlay.test.jsx +153 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/hooks.js +20 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/images/icon-depth.png +0 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/images/icon-light.png +0 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/images/search.svg +3 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/index.js +16 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/mockJsdom.js +8 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/styles.less +359 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/styles.less_old +201 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/utils.js +144 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/utils.test.js +88 -0
- package/src/components/manage/Blocks/index.js +2 -0
- package/src/express-middleware.js +37 -0
- package/src/index.js +29 -0
- package/theme/globals/site.overrides +12 -4
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import cx from 'classnames';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Map,
|
|
7
|
+
Layer,
|
|
8
|
+
Layers,
|
|
9
|
+
Controls,
|
|
10
|
+
useMapContext,
|
|
11
|
+
} from '@eeacms/volto-openlayers-map/api';
|
|
12
|
+
import { withOpenLayers } from '@eeacms/volto-openlayers-map';
|
|
13
|
+
|
|
14
|
+
import InfoOverlay from './InfoOverlay';
|
|
15
|
+
import FeatureInteraction from './FeatureInteraction';
|
|
16
|
+
import CaseStudyList from './CaseStudyListing';
|
|
17
|
+
|
|
18
|
+
import { centerAndResetMapZoom, getFeatures, scrollToElement } from './utils';
|
|
19
|
+
|
|
20
|
+
const styleCache = {};
|
|
21
|
+
const MapContextGateway = ({ setMap }) => {
|
|
22
|
+
const { map } = useMapContext();
|
|
23
|
+
React.useEffect(() => {
|
|
24
|
+
setMap(map);
|
|
25
|
+
}, [map, setMap]);
|
|
26
|
+
return null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function CaseStudyMap(props) {
|
|
30
|
+
const {
|
|
31
|
+
items,
|
|
32
|
+
activeItems,
|
|
33
|
+
hideFilters,
|
|
34
|
+
selectedCase,
|
|
35
|
+
onSelectedCase,
|
|
36
|
+
searchInput,
|
|
37
|
+
map,
|
|
38
|
+
setMap,
|
|
39
|
+
ol,
|
|
40
|
+
} = props;
|
|
41
|
+
const features = getFeatures({ cases: items, ol });
|
|
42
|
+
const [resetMapButtonClass, setResetMapButtonClass] =
|
|
43
|
+
React.useState('inactive');
|
|
44
|
+
|
|
45
|
+
const [tileWMSSources] = React.useState([
|
|
46
|
+
new ol.source.TileWMS({
|
|
47
|
+
url: 'https://gisco-services.ec.europa.eu/maps/service',
|
|
48
|
+
params: {
|
|
49
|
+
// LAYERS: 'OSMBlossomComposite', OSMCartoComposite, OSMPositronComposite
|
|
50
|
+
LAYERS: 'OSMPositronComposite',
|
|
51
|
+
TILED: true,
|
|
52
|
+
},
|
|
53
|
+
serverType: 'geoserver',
|
|
54
|
+
transition: 0,
|
|
55
|
+
}),
|
|
56
|
+
]);
|
|
57
|
+
const [pointsSource] = React.useState(
|
|
58
|
+
new ol.source.Vector({
|
|
59
|
+
features,
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const [clusterSource] = React.useState(
|
|
64
|
+
new ol.source.Cluster({
|
|
65
|
+
distance: 19,
|
|
66
|
+
source: pointsSource,
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
React.useEffect(() => {
|
|
71
|
+
if (activeItems) {
|
|
72
|
+
pointsSource.clear();
|
|
73
|
+
pointsSource.addFeatures(getFeatures({ cases: activeItems, ol }));
|
|
74
|
+
}
|
|
75
|
+
}, [activeItems, pointsSource, ol]);
|
|
76
|
+
|
|
77
|
+
React.useEffect(() => {
|
|
78
|
+
if (!map) return null;
|
|
79
|
+
|
|
80
|
+
const moveendListener = (e) => {
|
|
81
|
+
// console.log('map.getView()', map.getView());
|
|
82
|
+
// console.log('selectedCase', selectedCase);
|
|
83
|
+
const mapZoom = Math.round(map.getView().getZoom() * 10) / 10;
|
|
84
|
+
const mapCenter = map.getView().getCenter();
|
|
85
|
+
|
|
86
|
+
if (selectedCase) {
|
|
87
|
+
const coords = selectedCase.geometry.flatCoordinates;
|
|
88
|
+
const pixel = map.getPixelFromCoordinate(coords);
|
|
89
|
+
map.getInteractions().array_[9].getFeatures().clear();
|
|
90
|
+
map
|
|
91
|
+
.getInteractions()
|
|
92
|
+
.array_[9].getFeatures()
|
|
93
|
+
.push(map.getFeaturesAtPixel(pixel)[0]);
|
|
94
|
+
} else {
|
|
95
|
+
map.getInteractions().array_[9].getFeatures().clear();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (
|
|
99
|
+
mapZoom === 4 &&
|
|
100
|
+
JSON.stringify(mapCenter) ===
|
|
101
|
+
JSON.stringify(ol.proj.transform([10, 49], 'EPSG:4326', 'EPSG:3857'))
|
|
102
|
+
) {
|
|
103
|
+
setResetMapButtonClass('inactive');
|
|
104
|
+
} else {
|
|
105
|
+
setResetMapButtonClass('active');
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
map.on('moveend', moveendListener);
|
|
110
|
+
|
|
111
|
+
return () => {
|
|
112
|
+
map.un('moveend', moveendListener);
|
|
113
|
+
};
|
|
114
|
+
}, [map, selectedCase, resetMapButtonClass, setResetMapButtonClass, ol]);
|
|
115
|
+
|
|
116
|
+
const clusterStyle = React.useMemo(
|
|
117
|
+
() => selectedClusterStyle({ selectedCase, ol }),
|
|
118
|
+
[selectedCase, ol],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const MapWithSelection = React.useMemo(() => Map, []);
|
|
122
|
+
// console.log('render');
|
|
123
|
+
|
|
124
|
+
return features.length > 0 ? (
|
|
125
|
+
<div id="ol-map-container" className="nrr-case-study-map-container">
|
|
126
|
+
<MapWithSelection
|
|
127
|
+
view={{
|
|
128
|
+
center: ol.proj.fromLonLat([10, 49]),
|
|
129
|
+
showFullExtent: true,
|
|
130
|
+
zoom: 4,
|
|
131
|
+
}}
|
|
132
|
+
pixelRatio={1}
|
|
133
|
+
// controls={ol.control.defaults({ attribution: false })}
|
|
134
|
+
>
|
|
135
|
+
<Controls attribution={false} />
|
|
136
|
+
<Layers>
|
|
137
|
+
{hideFilters ? null : (
|
|
138
|
+
<button
|
|
139
|
+
className={cx(
|
|
140
|
+
'reset-map-button ui button secondary',
|
|
141
|
+
String(resetMapButtonClass),
|
|
142
|
+
)}
|
|
143
|
+
onClick={() => {
|
|
144
|
+
scrollToElement('search-input');
|
|
145
|
+
onSelectedCase(null);
|
|
146
|
+
centerAndResetMapZoom({ map, ol });
|
|
147
|
+
map.getInteractions().array_[9].getFeatures().clear();
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
<span className="result-info-title">Reset map</span>
|
|
151
|
+
<i className="icon ri-map-2-line"></i>
|
|
152
|
+
</button>
|
|
153
|
+
)}
|
|
154
|
+
<InfoOverlay
|
|
155
|
+
selectedFeature={selectedCase}
|
|
156
|
+
onFeatureSelect={onSelectedCase}
|
|
157
|
+
layerId={tileWMSSources[0]}
|
|
158
|
+
hideFilters={hideFilters}
|
|
159
|
+
/>
|
|
160
|
+
<FeatureInteraction
|
|
161
|
+
onFeatureSelect={onSelectedCase}
|
|
162
|
+
hideFilters={hideFilters}
|
|
163
|
+
selectedCase={selectedCase}
|
|
164
|
+
/>
|
|
165
|
+
<Layer.Tile source={tileWMSSources[0]} zIndex={0} />
|
|
166
|
+
<Layer.Vector
|
|
167
|
+
style={clusterStyle}
|
|
168
|
+
source={clusterSource}
|
|
169
|
+
zIndex={1}
|
|
170
|
+
/>
|
|
171
|
+
<MapContextGateway setMap={setMap} />
|
|
172
|
+
</Layers>
|
|
173
|
+
</MapWithSelection>
|
|
174
|
+
{hideFilters ? null : (
|
|
175
|
+
<CaseStudyList
|
|
176
|
+
map={map}
|
|
177
|
+
activeItems={activeItems}
|
|
178
|
+
selectedCase={selectedCase}
|
|
179
|
+
onSelectedCase={onSelectedCase}
|
|
180
|
+
pointsSource={pointsSource}
|
|
181
|
+
searchInput={searchInput}
|
|
182
|
+
/>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
) : null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const selectedClusterStyle = ({ selectedFeature, ol }) => {
|
|
189
|
+
function _clusterStyle(feature) {
|
|
190
|
+
const size = feature.get('features').length;
|
|
191
|
+
let style = styleCache[size];
|
|
192
|
+
|
|
193
|
+
if (!style) {
|
|
194
|
+
style = new ol.style.Style({
|
|
195
|
+
image: new ol.style.Circle({
|
|
196
|
+
radius: 12 + Math.min(Math.floor(size / 3), 10),
|
|
197
|
+
stroke: new ol.style.Stroke({
|
|
198
|
+
color: '#fff',
|
|
199
|
+
}),
|
|
200
|
+
fill: new ol.style.Fill({
|
|
201
|
+
color: '#007B6C', // #006BB8 #309ebc
|
|
202
|
+
}),
|
|
203
|
+
}),
|
|
204
|
+
text: new ol.style.Text({
|
|
205
|
+
text: size.toString(),
|
|
206
|
+
fill: new ol.style.Fill({
|
|
207
|
+
color: '#fff',
|
|
208
|
+
}),
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
styleCache[size] = style;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (size === 1) {
|
|
215
|
+
// let color = feature.values_.features[0].values_['color'];
|
|
216
|
+
let color = '#289588'; // #0083E0 #50B0A4
|
|
217
|
+
|
|
218
|
+
return new ol.style.Style({
|
|
219
|
+
image: new ol.style.Circle({
|
|
220
|
+
radius: 6,
|
|
221
|
+
fill: new ol.style.Fill({
|
|
222
|
+
color: '#fff',
|
|
223
|
+
}),
|
|
224
|
+
stroke: new ol.style.Stroke({
|
|
225
|
+
color: color,
|
|
226
|
+
width: 6,
|
|
227
|
+
}),
|
|
228
|
+
}),
|
|
229
|
+
});
|
|
230
|
+
} else {
|
|
231
|
+
return style;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return _clusterStyle;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export default withOpenLayers(CaseStudyMap);
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import CaseStudyMap from './CaseStudyMap';
|
|
5
|
+
import { getFeatures, centerAndResetMapZoom, scrollToElement } from './utils';
|
|
6
|
+
|
|
7
|
+
jest.mock('@eeacms/volto-openlayers-map', () => ({
|
|
8
|
+
withOpenLayers: (Comp) => Comp,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
jest.mock('@eeacms/volto-openlayers-map/api', () => ({
|
|
12
|
+
Map: ({ children }) => <div data-testid="mock-map">{children}</div>,
|
|
13
|
+
Layer: {
|
|
14
|
+
Tile: ({ source }) => <div data-testid="mock-tile-layer" />,
|
|
15
|
+
Vector: ({ source }) => <div data-testid="mock-vector-layer" />,
|
|
16
|
+
},
|
|
17
|
+
Layers: ({ children }) => <div data-testid="mock-layers">{children}</div>,
|
|
18
|
+
Controls: () => <div data-testid="mock-controls" />,
|
|
19
|
+
useMapContext: () => ({ map: mockMap }),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
// Mock child components so we don't render full DOM
|
|
23
|
+
jest.mock('./InfoOverlay', () => () => <div data-testid="mock-info-overlay" />);
|
|
24
|
+
jest.mock('./FeatureInteraction', () => () => (
|
|
25
|
+
<div data-testid="mock-feature-interaction" />
|
|
26
|
+
));
|
|
27
|
+
jest.mock('./CaseStudyListing', () => () => (
|
|
28
|
+
<div data-testid="mock-case-study-list" />
|
|
29
|
+
));
|
|
30
|
+
|
|
31
|
+
jest.mock('./utils', () => ({
|
|
32
|
+
getFeatures: jest.fn(),
|
|
33
|
+
centerAndResetMapZoom: jest.fn(),
|
|
34
|
+
scrollToElement: jest.fn(),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
const mockOl = {
|
|
38
|
+
source: {
|
|
39
|
+
TileWMS: jest.fn(() => ({ type: 'TileWMS' })),
|
|
40
|
+
Vector: jest.fn(() => ({
|
|
41
|
+
clear: jest.fn(),
|
|
42
|
+
addFeatures: jest.fn(),
|
|
43
|
+
})),
|
|
44
|
+
Cluster: jest.fn(() => ({})),
|
|
45
|
+
},
|
|
46
|
+
style: {
|
|
47
|
+
Style: jest.fn(() => ({})),
|
|
48
|
+
Circle: jest.fn(() => ({})),
|
|
49
|
+
Stroke: jest.fn(() => ({})),
|
|
50
|
+
Fill: jest.fn(() => ({})),
|
|
51
|
+
Text: jest.fn(() => ({})),
|
|
52
|
+
},
|
|
53
|
+
proj: {
|
|
54
|
+
transform: jest.fn(() => [111, 222]),
|
|
55
|
+
fromLonLat: jest.fn(() => [333, 444]),
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const mockMap = {
|
|
60
|
+
on: jest.fn(),
|
|
61
|
+
un: jest.fn(),
|
|
62
|
+
getView: jest.fn(() => ({
|
|
63
|
+
getZoom: () => 4,
|
|
64
|
+
getCenter: () => [111, 222],
|
|
65
|
+
})),
|
|
66
|
+
getInteractions: jest.fn(() => ({
|
|
67
|
+
array_: [
|
|
68
|
+
{},
|
|
69
|
+
{},
|
|
70
|
+
{},
|
|
71
|
+
{},
|
|
72
|
+
{},
|
|
73
|
+
{},
|
|
74
|
+
{},
|
|
75
|
+
{},
|
|
76
|
+
{},
|
|
77
|
+
{
|
|
78
|
+
getFeatures: jest.fn(() => ({
|
|
79
|
+
clear: jest.fn(),
|
|
80
|
+
push: jest.fn(),
|
|
81
|
+
})),
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
})),
|
|
85
|
+
getPixelFromCoordinate: jest.fn(() => [10, 20]),
|
|
86
|
+
getFeaturesAtPixel: jest.fn(() => ['f']),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
describe('CaseStudyMap', () => {
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
jest.clearAllMocks();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('renders nothing when no features', () => {
|
|
95
|
+
getFeatures.mockReturnValueOnce([]);
|
|
96
|
+
const { container } = render(
|
|
97
|
+
<CaseStudyMap items={[]} ol={mockOl} setMap={jest.fn()} />,
|
|
98
|
+
);
|
|
99
|
+
expect(container.firstChild).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('renders map and layers when features exist', () => {
|
|
103
|
+
getFeatures.mockReturnValueOnce([{ id: 1 }]);
|
|
104
|
+
render(
|
|
105
|
+
<CaseStudyMap
|
|
106
|
+
items={[{ id: 1 }]}
|
|
107
|
+
ol={mockOl}
|
|
108
|
+
setMap={jest.fn()}
|
|
109
|
+
map={mockMap}
|
|
110
|
+
onSelectedCase={jest.fn()}
|
|
111
|
+
/>,
|
|
112
|
+
);
|
|
113
|
+
expect(screen.getByTestId('mock-map')).toBeInTheDocument();
|
|
114
|
+
expect(screen.getByTestId('mock-tile-layer')).toBeInTheDocument();
|
|
115
|
+
expect(screen.getByTestId('mock-vector-layer')).toBeInTheDocument();
|
|
116
|
+
expect(screen.getByTestId('mock-case-study-list')).toBeInTheDocument();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('does not render CaseStudyList when hideFilters is true', () => {
|
|
120
|
+
getFeatures.mockReturnValueOnce([{ id: 1 }]);
|
|
121
|
+
render(
|
|
122
|
+
<CaseStudyMap
|
|
123
|
+
items={[{ id: 1 }]}
|
|
124
|
+
ol={mockOl}
|
|
125
|
+
hideFilters
|
|
126
|
+
setMap={jest.fn()}
|
|
127
|
+
map={mockMap}
|
|
128
|
+
onSelectedCase={jest.fn()}
|
|
129
|
+
/>,
|
|
130
|
+
);
|
|
131
|
+
expect(
|
|
132
|
+
screen.queryByTestId('mock-case-study-list'),
|
|
133
|
+
).not.toBeInTheDocument();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('clicking Reset map calls utils and clears features', () => {
|
|
137
|
+
getFeatures.mockReturnValueOnce([{ id: 1 }]);
|
|
138
|
+
const onSelectedCase = jest.fn();
|
|
139
|
+
render(
|
|
140
|
+
<CaseStudyMap
|
|
141
|
+
items={[{ id: 1 }]}
|
|
142
|
+
ol={mockOl}
|
|
143
|
+
setMap={jest.fn()}
|
|
144
|
+
map={mockMap}
|
|
145
|
+
onSelectedCase={onSelectedCase}
|
|
146
|
+
/>,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
fireEvent.click(screen.getByText('Reset map'));
|
|
150
|
+
expect(scrollToElement).toHaveBeenCalledWith('search-input');
|
|
151
|
+
expect(onSelectedCase).toHaveBeenCalledWith(null);
|
|
152
|
+
expect(centerAndResetMapZoom).toHaveBeenCalledWith({
|
|
153
|
+
map: mockMap,
|
|
154
|
+
ol: mockOl,
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('map moveend listener toggles resetMapButtonClass', () => {
|
|
159
|
+
getFeatures.mockReturnValueOnce([{ id: 1 }]);
|
|
160
|
+
render(
|
|
161
|
+
<CaseStudyMap
|
|
162
|
+
items={[{ id: 1 }]}
|
|
163
|
+
ol={mockOl}
|
|
164
|
+
setMap={jest.fn()}
|
|
165
|
+
map={mockMap}
|
|
166
|
+
onSelectedCase={jest.fn()}
|
|
167
|
+
/>,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const moveendCb = mockMap.on.mock.calls.find((c) => c[0] === 'moveend')[1];
|
|
171
|
+
|
|
172
|
+
moveendCb();
|
|
173
|
+
|
|
174
|
+
expect(mockMap.getView).toHaveBeenCalled();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export default function FeatureDisplay({ feature }) {
|
|
4
|
+
return feature ? (
|
|
5
|
+
<div id="csepopup">
|
|
6
|
+
<h3>
|
|
7
|
+
<strong>
|
|
8
|
+
<a target="_blank" rel="noopener noreferrer" href={feature.path}>
|
|
9
|
+
{feature.title}
|
|
10
|
+
</a>
|
|
11
|
+
</strong>
|
|
12
|
+
</h3>
|
|
13
|
+
{/* <div>
|
|
14
|
+
<h4>Measures implemented</h4>
|
|
15
|
+
<ul>
|
|
16
|
+
{feature.measures_implemented.map((item, index) => {
|
|
17
|
+
return (
|
|
18
|
+
<li key={index}>
|
|
19
|
+
<a
|
|
20
|
+
target="_blank"
|
|
21
|
+
rel="noopener noreferrer"
|
|
22
|
+
href={item['path']}
|
|
23
|
+
>
|
|
24
|
+
{item['title']}
|
|
25
|
+
</a>
|
|
26
|
+
</li>
|
|
27
|
+
);
|
|
28
|
+
})}
|
|
29
|
+
</ul>
|
|
30
|
+
</div> */}
|
|
31
|
+
{/* <div>
|
|
32
|
+
<h4>typology_of_measures </h4>
|
|
33
|
+
<ul>
|
|
34
|
+
{feature.typology_of_measures.map((item, index) => {
|
|
35
|
+
return <li key={index}>{item}</li>;
|
|
36
|
+
})}
|
|
37
|
+
</ul>
|
|
38
|
+
</div> */}
|
|
39
|
+
</div>
|
|
40
|
+
) : null;
|
|
41
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom/extend-expect';
|
|
4
|
+
|
|
5
|
+
import FeatureDisplay from './FeatureDisplay';
|
|
6
|
+
|
|
7
|
+
describe('FeatureDisplay Component', () => {
|
|
8
|
+
it('renders correctly with feature data', () => {
|
|
9
|
+
const feature = {
|
|
10
|
+
title: 'Sample Feature',
|
|
11
|
+
path: 'https://example.com/sample-feature',
|
|
12
|
+
measures_implemented: [
|
|
13
|
+
{ path: 'https://example.com/nwrm-1', title: 'NWRM 1' },
|
|
14
|
+
{ path: 'https://example.com/nwrm-2', title: 'NWRM 2' },
|
|
15
|
+
],
|
|
16
|
+
typology_of_measures: ['Sector 1', 'Sector 2'],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const { getByRole } = render(<FeatureDisplay feature={feature} />);
|
|
20
|
+
|
|
21
|
+
const titleLink = getByRole('link', { name: 'Sample Feature' });
|
|
22
|
+
expect(titleLink).toHaveAttribute(
|
|
23
|
+
'href',
|
|
24
|
+
'https://example.com/sample-feature',
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('renders nothing when feature is null', () => {
|
|
29
|
+
const { container } = render(<FeatureDisplay feature={null} />);
|
|
30
|
+
expect(container.firstChild).toBeNull();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { withOpenLayers } from '@eeacms/volto-openlayers-map';
|
|
3
|
+
import { useMapContext } from '@eeacms/volto-openlayers-map/api';
|
|
4
|
+
import { scrollToElement, zoomMapToFeatures } from './utils';
|
|
5
|
+
|
|
6
|
+
export const useStyles = ({ ol }) => {
|
|
7
|
+
const selected = React.useMemo(
|
|
8
|
+
() =>
|
|
9
|
+
new ol.style.Style({
|
|
10
|
+
image: new ol.style.Circle({
|
|
11
|
+
radius: 12,
|
|
12
|
+
fill: new ol.style.Fill({
|
|
13
|
+
color: '#ccc',
|
|
14
|
+
}),
|
|
15
|
+
stroke: new ol.style.Stroke({
|
|
16
|
+
color: '#fff',
|
|
17
|
+
width: 0,
|
|
18
|
+
}),
|
|
19
|
+
}),
|
|
20
|
+
}),
|
|
21
|
+
[ol],
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const selectStyle = React.useCallback(
|
|
25
|
+
(feature) => {
|
|
26
|
+
// const color = feature.values_.features[0].values_['color'] || '#ccc';
|
|
27
|
+
const color = '#0A99FF'; // #004B7F #309ebc #0A99FF
|
|
28
|
+
// console.log(color);
|
|
29
|
+
selected.image_.getFill().setColor(color);
|
|
30
|
+
return selected;
|
|
31
|
+
},
|
|
32
|
+
[selected],
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return { selected, selectStyle };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function FeatureInteraction({
|
|
39
|
+
onFeatureSelect,
|
|
40
|
+
hideFilters,
|
|
41
|
+
selectedCase,
|
|
42
|
+
ol,
|
|
43
|
+
}) {
|
|
44
|
+
// console.log('featureinteraction', selectedCase);
|
|
45
|
+
const { map } = useMapContext();
|
|
46
|
+
const { selectStyle } = useStyles({ ol });
|
|
47
|
+
|
|
48
|
+
const select = new ol.interaction.Select({
|
|
49
|
+
condition: ol.condition.click,
|
|
50
|
+
style: hideFilters ? null : selectStyle,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
React.useEffect(() => {
|
|
54
|
+
if (!map) return;
|
|
55
|
+
|
|
56
|
+
select.on('select', function (e) {
|
|
57
|
+
const features = e.target.getFeatures().getArray();
|
|
58
|
+
|
|
59
|
+
features.forEach((feature) => {
|
|
60
|
+
const subfeatures = feature.values_.features;
|
|
61
|
+
if (subfeatures.length === 1) {
|
|
62
|
+
const selectedFeature = subfeatures[0].values_;
|
|
63
|
+
if (hideFilters) {
|
|
64
|
+
const url = window.location.origin + selectedFeature.path;
|
|
65
|
+
// window.open(url);
|
|
66
|
+
window.location.href = url;
|
|
67
|
+
}
|
|
68
|
+
onFeatureSelect(selectedFeature);
|
|
69
|
+
scrollToElement('ol-map-container');
|
|
70
|
+
// map.getView().animate({
|
|
71
|
+
// duration: 10,
|
|
72
|
+
// center: selectedFeature.geometry.flatCoordinates,
|
|
73
|
+
// });
|
|
74
|
+
} else {
|
|
75
|
+
onFeatureSelect(null);
|
|
76
|
+
zoomMapToFeatures({ map, features: subfeatures, ol });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return null;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
map.addInteraction(select);
|
|
84
|
+
|
|
85
|
+
map.on('pointermove', (e) => {
|
|
86
|
+
const pixel = map.getEventPixel(e.originalEvent);
|
|
87
|
+
const hit = map.hasFeatureAtPixel(pixel);
|
|
88
|
+
map.getViewport().style.cursor = hit ? 'pointer' : '';
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return () => map.removeInteraction(select);
|
|
92
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
93
|
+
}, [map, selectStyle, onFeatureSelect, hideFilters]);
|
|
94
|
+
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default withOpenLayers(FeatureInteraction);
|