@eeacms/volto-cca-policy 0.2.69 → 0.2.70
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 +28 -8
- package/package.json +1 -1
- package/src/components/Result/ClusterHorizontalCardItem.jsx +136 -0
- package/src/components/manage/Blocks/C3SIndicatorsGlossaryBlock/C3SIndicatorsGlossaryBlockView.test.jsx +34 -0
- package/src/components/manage/Blocks/C3SIndicatorsListingBlock/C3SIndicatorsListingBlockView.test.jsx +43 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyExplorerEdit.test.jsx +89 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyExplorerView.test.jsx +93 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMap.jsx +13 -79
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMap.test.jsx +131 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/FeatureDisplay.jsx +5 -1
- package/src/components/manage/Blocks/CaseStudyExplorer/FeatureDisplay.test.jsx +59 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/FeatureInteraction.jsx +132 -53
- package/src/components/manage/Blocks/CaseStudyExplorer/styles.less +4 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/useInteractiveStyles.jsx +189 -0
- package/src/components/manage/Blocks/MKHMap/InfoOverlay.jsx +20 -8
- package/src/components/manage/Blocks/MKHMap/View.jsx +1 -1
- package/src/components/theme/ImageGallery/styles.less +1 -1
- package/src/index.js +4 -1
- package/src/search/config.js +79 -26
- package/src/search/facets.js +12 -1
- package/src/search/views.js +15 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/InfoOverlay.jsx +0 -80
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
3
|
+
import configureStore from 'redux-mock-store';
|
|
4
|
+
import { render } from '@testing-library/react';
|
|
5
|
+
import '@testing-library/jest-dom/extend-expect';
|
|
6
|
+
import { Provider } from 'react-intl-redux';
|
|
7
|
+
import CaseStudyMap from './CaseStudyMap';
|
|
8
|
+
import * as utils from './utils';
|
|
9
|
+
|
|
10
|
+
const mockStore = configureStore();
|
|
11
|
+
|
|
12
|
+
jest.mock('./utils', () => ({
|
|
13
|
+
getFeatures: jest.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
// Mock the OpenLayers API
|
|
17
|
+
jest.mock('@eeacms/volto-openlayers-map/api', () => ({
|
|
18
|
+
Map: jest.fn(({ children }) => <div>{children}</div>),
|
|
19
|
+
Layer: {
|
|
20
|
+
Tile: jest.fn(() => <div />),
|
|
21
|
+
},
|
|
22
|
+
Layers: jest.fn(({ children }) => <div>{children}</div>),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
// Mock additional OpenLayers functionality
|
|
26
|
+
jest.mock('@eeacms/volto-openlayers-map', () => ({
|
|
27
|
+
openlayers: {
|
|
28
|
+
source: {
|
|
29
|
+
TileWMS: jest.fn().mockImplementation(() => ({})),
|
|
30
|
+
},
|
|
31
|
+
proj: {
|
|
32
|
+
fromLonLat: jest.fn().mockReturnValue([0, 0]),
|
|
33
|
+
},
|
|
34
|
+
control: {
|
|
35
|
+
defaults: jest.fn().mockReturnValue([]),
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
jest.mock('./FeatureInteraction', () => () => (
|
|
41
|
+
<div>Mock Feature Interaction</div>
|
|
42
|
+
));
|
|
43
|
+
|
|
44
|
+
const store = mockStore({
|
|
45
|
+
userSession: { token: '1234' },
|
|
46
|
+
intl: {
|
|
47
|
+
locale: 'en',
|
|
48
|
+
messages: {},
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('CaseStudyMap', () => {
|
|
53
|
+
it('renders the component with features', () => {
|
|
54
|
+
const data = {
|
|
55
|
+
activeItems: [
|
|
56
|
+
{
|
|
57
|
+
geometry: {
|
|
58
|
+
color: '#009900',
|
|
59
|
+
svg: { fill_color: '#009900' },
|
|
60
|
+
type: 'Point',
|
|
61
|
+
coordinates: [4.19059753418, 52.0476343911],
|
|
62
|
+
},
|
|
63
|
+
properties: {
|
|
64
|
+
impacts: ',FLOODING,SEALEVELRISE,STORM,',
|
|
65
|
+
title: 'Sand Motor',
|
|
66
|
+
portal_type: 'casestudy',
|
|
67
|
+
description:
|
|
68
|
+
'<p>The Sand Motor is a mega-nourishment implemented in the Delfland Coast.</p>',
|
|
69
|
+
url: 'http://website.com',
|
|
70
|
+
adaptation_options_links:
|
|
71
|
+
"<a href='http://website.com'>Beach and shoreface nourishment</a>",
|
|
72
|
+
elements_str: 'Nature-based solutions',
|
|
73
|
+
image: 'http://website.com/preview',
|
|
74
|
+
origin_adaptecca: 20,
|
|
75
|
+
adaptation_options: 'Dune construction',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
items: [
|
|
80
|
+
{
|
|
81
|
+
geometry: {
|
|
82
|
+
color: '#009900',
|
|
83
|
+
svg: { fill_color: '#009900' },
|
|
84
|
+
type: 'Point',
|
|
85
|
+
coordinates: [4.19059753418, 52.0476343911],
|
|
86
|
+
},
|
|
87
|
+
properties: {
|
|
88
|
+
impacts: ',FLOODING,SEALEVELRISE,STORM,',
|
|
89
|
+
title: 'Sand Motor',
|
|
90
|
+
portal_type: 'casestudy',
|
|
91
|
+
description:
|
|
92
|
+
'<p>The Sand Motor is a mega-nourishment implemented in the Delfland Coast.</p>',
|
|
93
|
+
url: 'http://website.com',
|
|
94
|
+
adaptation_options_links:
|
|
95
|
+
"<a href='http://website.com'>Beach and shoreface nourishment</a>",
|
|
96
|
+
elements_str: 'Nature-based solutions',
|
|
97
|
+
image: 'http://website.com/preview',
|
|
98
|
+
origin_adaptecca: 20,
|
|
99
|
+
adaptation_options: 'Dune construction',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
utils.getFeatures.mockReturnValueOnce(data.items); // Mock getFeatures to return data
|
|
106
|
+
|
|
107
|
+
const { container } = render(
|
|
108
|
+
<Provider store={store}>
|
|
109
|
+
<MemoryRouter>
|
|
110
|
+
<CaseStudyMap {...data} />
|
|
111
|
+
</MemoryRouter>
|
|
112
|
+
</Provider>,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
expect(container).toBeTruthy(); // Ensure the component renders correctly
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('does not render the component when there are no features', () => {
|
|
119
|
+
utils.getFeatures.mockReturnValueOnce([]); // Mock getFeatures to return an empty array
|
|
120
|
+
|
|
121
|
+
const { container } = render(
|
|
122
|
+
<Provider store={store}>
|
|
123
|
+
<MemoryRouter>
|
|
124
|
+
<CaseStudyMap activeItems={[]} items={[]} />
|
|
125
|
+
</MemoryRouter>
|
|
126
|
+
</Provider>,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(container).toBeEmptyDOMElement(); // Expect the container to be empty
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -5,7 +5,11 @@ export default function FeatureDisplay({ feature }) {
|
|
|
5
5
|
return feature ? (
|
|
6
6
|
<div id="csepopup">
|
|
7
7
|
<p>
|
|
8
|
-
<strong>
|
|
8
|
+
<strong>
|
|
9
|
+
<a className="dbitem" href={feature.url} target="_blank">
|
|
10
|
+
{feature.title}
|
|
11
|
+
</a>
|
|
12
|
+
</strong>
|
|
9
13
|
</p>
|
|
10
14
|
<span className="img">
|
|
11
15
|
<center>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
3
|
+
import configureStore from 'redux-mock-store';
|
|
4
|
+
import { render } from '@testing-library/react';
|
|
5
|
+
import '@testing-library/jest-dom/extend-expect';
|
|
6
|
+
import { Provider } from 'react-intl-redux';
|
|
7
|
+
import FeatureDisplay from './FeatureDisplay';
|
|
8
|
+
|
|
9
|
+
const mockStore = configureStore();
|
|
10
|
+
|
|
11
|
+
describe('FeatureDisplay', () => {
|
|
12
|
+
it('should render the component', () => {
|
|
13
|
+
const feature = {
|
|
14
|
+
title: 'Case study',
|
|
15
|
+
url: 'http://example.com',
|
|
16
|
+
image: 'http://example.com/image.jpg',
|
|
17
|
+
impacts: 'Impact A, Impact B',
|
|
18
|
+
adaptation_options_links: '',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const store = mockStore({
|
|
22
|
+
userSession: { token: '1234' },
|
|
23
|
+
intl: {
|
|
24
|
+
locale: 'en',
|
|
25
|
+
messages: {},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const { container } = render(
|
|
30
|
+
<Provider store={store}>
|
|
31
|
+
<MemoryRouter>
|
|
32
|
+
<FeatureDisplay feature={feature} />
|
|
33
|
+
</MemoryRouter>
|
|
34
|
+
</Provider>,
|
|
35
|
+
);
|
|
36
|
+
expect(container).toBeTruthy();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should not render the component', () => {
|
|
40
|
+
const feature = null;
|
|
41
|
+
|
|
42
|
+
const store = mockStore({
|
|
43
|
+
userSession: { token: '1234' },
|
|
44
|
+
intl: {
|
|
45
|
+
locale: 'en',
|
|
46
|
+
messages: {},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const { container } = render(
|
|
51
|
+
<Provider store={store}>
|
|
52
|
+
<MemoryRouter>
|
|
53
|
+
<FeatureDisplay feature={feature} />
|
|
54
|
+
</MemoryRouter>
|
|
55
|
+
</Provider>,
|
|
56
|
+
);
|
|
57
|
+
expect(container).toBeTruthy();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -1,52 +1,73 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { openlayers as ol } from '@eeacms/volto-openlayers-map';
|
|
3
3
|
import { useMapContext } from '@eeacms/volto-openlayers-map/api';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
4
|
+
import FeatureDisplay from './FeatureDisplay';
|
|
5
|
+
import { getFeatures } from './utils';
|
|
6
|
+
import {
|
|
7
|
+
clusterStyle,
|
|
8
|
+
useStyles,
|
|
9
|
+
getExtentOfFeatures,
|
|
10
|
+
setClicked,
|
|
11
|
+
} from './useInteractiveStyles';
|
|
12
|
+
|
|
13
|
+
export default function FeatureInteraction({
|
|
14
|
+
selectedFeature,
|
|
15
|
+
onFeatureSelect,
|
|
16
|
+
mapCenter,
|
|
17
|
+
features,
|
|
18
|
+
activeItems,
|
|
19
|
+
}) {
|
|
20
|
+
const { map } = useMapContext();
|
|
21
|
+
const [clusterLayer, setClusterLayer] = React.useState();
|
|
22
|
+
const [clusterCirclesLayer, setClusterCirclesLayer] = React.useState();
|
|
23
|
+
const { selectStyle, clusterCircleStyle } = useStyles();
|
|
24
|
+
|
|
25
|
+
const [pointsSource] = React.useState(
|
|
26
|
+
new ol.source.Vector({
|
|
27
|
+
features,
|
|
28
|
+
}),
|
|
21
29
|
);
|
|
22
30
|
|
|
23
|
-
const
|
|
24
|
-
(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
selected.image_.getFill().setColor(color);
|
|
29
|
-
return selected;
|
|
30
|
-
},
|
|
31
|
-
[selected],
|
|
31
|
+
const [clusterSource] = React.useState(
|
|
32
|
+
new ol.source.Cluster({
|
|
33
|
+
distance: 50,
|
|
34
|
+
source: pointsSource,
|
|
35
|
+
}),
|
|
32
36
|
);
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return point.getExtent();
|
|
41
|
-
}
|
|
38
|
+
React.useEffect(() => {
|
|
39
|
+
if (activeItems) {
|
|
40
|
+
pointsSource.clear();
|
|
41
|
+
pointsSource.addFeatures(getFeatures(activeItems));
|
|
42
|
+
}
|
|
43
|
+
}, [activeItems, pointsSource]);
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const { selectStyle } = useStyles();
|
|
45
|
+
const [isClient, setIsClient] = React.useState(false);
|
|
46
|
+
React.useEffect(() => setIsClient(true), []);
|
|
46
47
|
|
|
48
|
+
// form the clusters layer
|
|
47
49
|
React.useEffect(() => {
|
|
48
50
|
if (!map) return;
|
|
49
51
|
|
|
52
|
+
// Layer displaying the expanded view of overlapping cluster members.
|
|
53
|
+
const clusterCirclesLayer = new ol.layer.Vector({
|
|
54
|
+
source: clusterSource,
|
|
55
|
+
style: clusterCircleStyle,
|
|
56
|
+
});
|
|
57
|
+
setClusterCirclesLayer(clusterCirclesLayer);
|
|
58
|
+
map.addLayer(clusterCirclesLayer);
|
|
59
|
+
|
|
60
|
+
const clusterLayer = new ol.layer.Vector({
|
|
61
|
+
source: clusterSource,
|
|
62
|
+
style: clusterStyle,
|
|
63
|
+
});
|
|
64
|
+
setClusterLayer(clusterLayer);
|
|
65
|
+
map.addLayer(clusterLayer);
|
|
66
|
+
}, [map, clusterSource, clusterCircleStyle]);
|
|
67
|
+
|
|
68
|
+
React.useEffect(() => {
|
|
69
|
+
if (!(map && clusterLayer)) return;
|
|
70
|
+
|
|
50
71
|
const select = new ol.interaction.Select({
|
|
51
72
|
condition: ol.condition.click,
|
|
52
73
|
style: selectStyle,
|
|
@@ -54,6 +75,10 @@ export default function FeatureInteraction({ onFeatureSelect }) {
|
|
|
54
75
|
|
|
55
76
|
select.on('select', function (e) {
|
|
56
77
|
const features = e.target.getFeatures().getArray();
|
|
78
|
+
// const pixel = e.mapBrowserEvent.pixel;
|
|
79
|
+
// clusterLayer.getFeatures(pixel).then((fs) => {
|
|
80
|
+
// console.log('fs', { fs, features });
|
|
81
|
+
// });
|
|
57
82
|
|
|
58
83
|
features.forEach((feature) => {
|
|
59
84
|
const subfeatures = feature.values_.features;
|
|
@@ -64,33 +89,87 @@ export default function FeatureInteraction({ onFeatureSelect }) {
|
|
|
64
89
|
onFeatureSelect(selectedFeature);
|
|
65
90
|
const paddedExtent = ol.extent.buffer(extent, 50000);
|
|
66
91
|
|
|
67
|
-
|
|
92
|
+
const view = map.getView();
|
|
93
|
+
view.fit(paddedExtent, { ...map.getSize(), duration: 1000 });
|
|
68
94
|
} else {
|
|
69
95
|
// zoom to extent of cluster points
|
|
96
|
+
|
|
97
|
+
const view = map.getView();
|
|
98
|
+
const resolution = view.getResolution();
|
|
70
99
|
const extent = getExtentOfFeatures(subfeatures);
|
|
71
100
|
|
|
72
|
-
|
|
73
|
-
(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
101
|
+
if (
|
|
102
|
+
view.getZoom() === view.getMaxZoom() ||
|
|
103
|
+
(ol.extent.getWidth(extent) < resolution &&
|
|
104
|
+
ol.extent.getHeight(extent) < resolution)
|
|
105
|
+
) {
|
|
106
|
+
// console.log('set cluster circles style', features[0]);
|
|
107
|
+
setClicked(features[0], resolution);
|
|
108
|
+
clusterCirclesLayer.setStyle(clusterCircleStyle);
|
|
109
|
+
} else {
|
|
110
|
+
let extentBuffer =
|
|
111
|
+
(extent[3] - extent[1] + extent[2] - extent[0]) / 4;
|
|
112
|
+
extentBuffer = extentBuffer < 500 ? 500 : extentBuffer;
|
|
113
|
+
const paddedExtent = ol.extent.buffer(extent, extentBuffer);
|
|
114
|
+
map
|
|
115
|
+
.getView()
|
|
116
|
+
.fit(paddedExtent, { ...map.getSize(), duration: 1000 });
|
|
117
|
+
}
|
|
77
118
|
}
|
|
78
119
|
});
|
|
79
|
-
|
|
80
|
-
return null;
|
|
81
120
|
});
|
|
82
121
|
|
|
83
|
-
|
|
122
|
+
function handleClick(evt) {
|
|
123
|
+
if (evt.originalEvent.target.tagName === 'A') return;
|
|
124
|
+
if (selectedFeature) {
|
|
125
|
+
onFeatureSelect(null);
|
|
84
126
|
|
|
85
|
-
|
|
86
|
-
|
|
127
|
+
const view = map.getView();
|
|
128
|
+
view.animate({
|
|
129
|
+
...mapCenter,
|
|
130
|
+
duration: 1000,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function handlePointerMove(e) {
|
|
87
136
|
const pixel = map.getEventPixel(e.originalEvent);
|
|
88
137
|
const hit = map.hasFeatureAtPixel(pixel);
|
|
89
138
|
map.getViewport().style.cursor = hit ? 'pointer' : '';
|
|
90
|
-
}
|
|
139
|
+
}
|
|
91
140
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
141
|
+
map.addInteraction(select);
|
|
142
|
+
map.on('click', handleClick);
|
|
143
|
+
map.on('pointermove', handlePointerMove);
|
|
144
|
+
|
|
145
|
+
return () => {
|
|
146
|
+
map.removeInteraction(select);
|
|
147
|
+
map.un('click', handleClick);
|
|
148
|
+
map.un('pointermove', handlePointerMove);
|
|
149
|
+
};
|
|
150
|
+
}, [
|
|
151
|
+
map,
|
|
152
|
+
selectStyle,
|
|
153
|
+
onFeatureSelect,
|
|
154
|
+
selectedFeature,
|
|
155
|
+
mapCenter,
|
|
156
|
+
clusterLayer,
|
|
157
|
+
clusterCircleStyle,
|
|
158
|
+
clusterCirclesLayer,
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
return isClient ? (
|
|
162
|
+
<div
|
|
163
|
+
id="popup-overlay"
|
|
164
|
+
style={{
|
|
165
|
+
position: 'absolute',
|
|
166
|
+
bottom: '30px',
|
|
167
|
+
right: '30px',
|
|
168
|
+
zIndex: 1,
|
|
169
|
+
visibility: selectedFeature ? 'visible' : 'hidden',
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
{selectedFeature ? <FeatureDisplay feature={selectedFeature} /> : null}
|
|
173
|
+
</div>
|
|
174
|
+
) : null;
|
|
96
175
|
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { openlayers as ol } from '@eeacms/volto-openlayers-map';
|
|
3
|
+
const _cached = {};
|
|
4
|
+
|
|
5
|
+
function getStyle(size, haveAdaptecca) {
|
|
6
|
+
let style = _cached[size + '_' + haveAdaptecca];
|
|
7
|
+
|
|
8
|
+
if (!style) {
|
|
9
|
+
style =
|
|
10
|
+
size === 1
|
|
11
|
+
? new ol.style.Style({
|
|
12
|
+
image: new ol.style.Circle({
|
|
13
|
+
radius: 5,
|
|
14
|
+
stroke: new ol.style.Stroke({
|
|
15
|
+
color: '#fff',
|
|
16
|
+
}),
|
|
17
|
+
fill: new ol.style.Fill({
|
|
18
|
+
color: haveAdaptecca ? '#00ffff' : '#005c96',
|
|
19
|
+
}),
|
|
20
|
+
}),
|
|
21
|
+
})
|
|
22
|
+
: new ol.style.Style({
|
|
23
|
+
image: new ol.style.Circle({
|
|
24
|
+
radius: 10 + Math.min(Math.floor(size / 3), 10),
|
|
25
|
+
stroke: new ol.style.Stroke({
|
|
26
|
+
color: '#fff',
|
|
27
|
+
}),
|
|
28
|
+
fill: new ol.style.Fill({
|
|
29
|
+
color: haveAdaptecca ? '#318CE7' : '#005c96',
|
|
30
|
+
}),
|
|
31
|
+
}),
|
|
32
|
+
text: new ol.style.Text({
|
|
33
|
+
text: size.toString(),
|
|
34
|
+
fill: new ol.style.Fill({
|
|
35
|
+
color: '#fff',
|
|
36
|
+
}),
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
_cached[size + '_' + haveAdaptecca] = style;
|
|
40
|
+
}
|
|
41
|
+
return style;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function useStyles() {
|
|
45
|
+
const selectStyle = React.useCallback((feature) => {
|
|
46
|
+
const selected = new ol.style.Style({
|
|
47
|
+
image: new ol.style.Circle({
|
|
48
|
+
radius: 12,
|
|
49
|
+
fill: new ol.style.Fill({
|
|
50
|
+
color: '#005c96',
|
|
51
|
+
}),
|
|
52
|
+
stroke: new ol.style.Stroke({
|
|
53
|
+
color: 'rgba(0, 92, 150, 0.9)',
|
|
54
|
+
width: 2,
|
|
55
|
+
}),
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
// const color = feature.get('COLOR') || '#eeeeee';
|
|
59
|
+
// selected.getFill().setColor(color);
|
|
60
|
+
const color = feature.values_.features[0].values_['color'] || '#ccc';
|
|
61
|
+
selected.image_.getFill().setColor(color);
|
|
62
|
+
return selected;
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
const convexHullStroke = React.useMemo(
|
|
66
|
+
() =>
|
|
67
|
+
new ol.style.Stroke({
|
|
68
|
+
color: 'rgba(204, 85, 0, 1)',
|
|
69
|
+
width: 1.5,
|
|
70
|
+
}),
|
|
71
|
+
[],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const clusterMemberStyle = React.useMemo(() => {
|
|
75
|
+
const darkIcon = new ol.style.Icon({
|
|
76
|
+
src: 'data/icons/emoticon-cool.svg',
|
|
77
|
+
});
|
|
78
|
+
const lightIcon = new ol.style.Icon({
|
|
79
|
+
src: 'data/icons/emoticon-cool-outline.svg',
|
|
80
|
+
});
|
|
81
|
+
const _clusterMemberStyle = (clusterMember) => {
|
|
82
|
+
return new ol.style.Style({
|
|
83
|
+
geometry: clusterMember.getGeometry(),
|
|
84
|
+
image: clusterMember.get('LEISTUNG') > 5 ? darkIcon : lightIcon,
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
return _clusterMemberStyle;
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
const clusterCircleStyle = React.useCallback(
|
|
91
|
+
(cluster, resolution) => {
|
|
92
|
+
// if (cluster?.ol_uid) {
|
|
93
|
+
// console.log(cluster?.ol_uid, cluster);
|
|
94
|
+
// }
|
|
95
|
+
// console.log('clusterCircleStyle', {
|
|
96
|
+
// cluster,
|
|
97
|
+
// resolution,
|
|
98
|
+
// clickFeature,
|
|
99
|
+
// clickResolution,
|
|
100
|
+
// });
|
|
101
|
+
if (cluster !== clickFeature || resolution !== clickResolution) {
|
|
102
|
+
// console.log('return null', {
|
|
103
|
+
// cluster,
|
|
104
|
+
// clickFeature,
|
|
105
|
+
// resolution,
|
|
106
|
+
// clickResolution,
|
|
107
|
+
// });
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// console.log('members', cluster);
|
|
112
|
+
const clusterMembers = cluster.get('features');
|
|
113
|
+
const centerCoordinates = cluster.getGeometry().getCoordinates();
|
|
114
|
+
return generatePointsCircle(
|
|
115
|
+
clusterMembers.length,
|
|
116
|
+
cluster.getGeometry().getCoordinates(),
|
|
117
|
+
resolution,
|
|
118
|
+
).reduce((styles, coordinates, i) => {
|
|
119
|
+
const point = new ol.geom.Point(coordinates);
|
|
120
|
+
const line = new ol.geom.LineString([centerCoordinates, coordinates]);
|
|
121
|
+
styles.unshift(
|
|
122
|
+
new ol.style.Style({
|
|
123
|
+
geometry: line,
|
|
124
|
+
stroke: convexHullStroke,
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
styles.push(
|
|
128
|
+
clusterMemberStyle(
|
|
129
|
+
new ol.feature.Feature({
|
|
130
|
+
...clusterMembers[i].getProperties(),
|
|
131
|
+
geometry: point,
|
|
132
|
+
}),
|
|
133
|
+
),
|
|
134
|
+
);
|
|
135
|
+
return styles;
|
|
136
|
+
}, []);
|
|
137
|
+
},
|
|
138
|
+
[convexHullStroke, clusterMemberStyle],
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return { clusterCircleStyle, selectStyle };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let clickFeature, clickResolution;
|
|
145
|
+
|
|
146
|
+
export function setClicked(feature, resolution) {
|
|
147
|
+
clickFeature = feature;
|
|
148
|
+
clickResolution = resolution;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function generatePointsCircle(count, clusterCenter, resolution) {
|
|
152
|
+
const circumference =
|
|
153
|
+
circleDistanceMultiplier * circleFootSeparation * (2 + count);
|
|
154
|
+
let legLength = circumference / (Math.PI * 2); //radius from circumference
|
|
155
|
+
const angleStep = (Math.PI * 2) / count;
|
|
156
|
+
const res = [];
|
|
157
|
+
let angle;
|
|
158
|
+
|
|
159
|
+
legLength = Math.max(legLength, 35) * resolution; // Minimum distance to get outside the cluster icon.
|
|
160
|
+
|
|
161
|
+
for (let i = 0; i < count; ++i) {
|
|
162
|
+
// Clockwise, like spiral.
|
|
163
|
+
angle = circleStartAngle + i * angleStep;
|
|
164
|
+
res.push([
|
|
165
|
+
clusterCenter[0] + legLength * Math.cos(angle),
|
|
166
|
+
clusterCenter[1] + legLength * Math.sin(angle),
|
|
167
|
+
]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return res;
|
|
171
|
+
}
|
|
172
|
+
export function clusterStyle(feature) {
|
|
173
|
+
const size = feature.get('features').length;
|
|
174
|
+
const cases = feature
|
|
175
|
+
.get('features')
|
|
176
|
+
.filter((_case) => _case['values_']['origin_adaptecca'] < 20);
|
|
177
|
+
|
|
178
|
+
return getStyle(size, cases.length > 0 ? 1 : 0);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function getExtentOfFeatures(features) {
|
|
182
|
+
const points = features.map((f) => f.getGeometry().flatCoordinates);
|
|
183
|
+
const point = new ol.geom.MultiPoint(points);
|
|
184
|
+
return point.getExtent();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const circleDistanceMultiplier = 1;
|
|
188
|
+
const circleFootSeparation = 28;
|
|
189
|
+
const circleStartAngle = Math.PI / 2;
|