Skip to content

Commit 6ff71ad

Browse files
committed
implement unselectable features
1 parent 465a24d commit 6ff71ad

File tree

12 files changed

+180
-13
lines changed

12 files changed

+180
-13
lines changed

packages/clients/snowbox/src/language.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,18 @@ const language: LanguageOption[] = [
1212
'Strecken U-Bahn © Freie und Hansestadt Hamburg, Behörde für Wirtschaft, Verkehr und Innovation',
1313
rapid:
1414
'Strecken S-Bahn © Freie und Hansestadt Hamburg, Behörde für Wirtschaft, Verkehr und Innovation',
15+
reports: 'Meldungen durch Bürger',
16+
ausgleichsflaechen:
17+
'Ausgleichsflächen © Freie und Hansestadt Hamburg, Behörde für Umwelt und Energie',
1518
hamburgBorder: 'Landesgrenze Hamburg © Freie und Hansestadt Hamburg',
1619
},
1720
layers: {
1821
basemap: 'Basemap.de (Farbe)',
1922
basemapGrey: 'Basemap.de (Grau)',
2023
underground: 'U-Bahn',
2124
rapid: 'S-Bahn',
25+
reports: 'Anliegen (MML)',
26+
ausgleichsflaechen: 'Ausgleichsflächen',
2227
hamburgBorder: 'Landesgrenze Hamburg',
2328
},
2429
},
@@ -43,13 +48,18 @@ const language: LanguageOption[] = [
4348
'Railway Lines U-Bahn © Freie und Hansestadt Hamburg, Behörde für Wirtschaft, Verkehr und Innovation',
4449
rapid:
4550
'Railway Lines S-Bahn © Freie und Hansestadt Hamburg, Behörde für Wirtschaft, Verkehr und Innovation',
51+
reports: 'Reports by citizens',
52+
ausgleichsflaechen:
53+
'Compensation area © Freie und Hansestadt Hamburg, Behörde für Umwelt und Energie',
4654
hamburgBorder: 'City border Hamburg © Freie und Hansestadt Hamburg',
4755
},
4856
layers: {
4957
basemap: 'Basemap.de (Coloured)',
5058
basemapGrey: 'Basemap.de (Grey)',
5159
underground: 'Underground railway (U-Bahn)',
5260
rapid: 'City rapid railway (S-Bahn)',
61+
reports: 'Reports (MML)',
62+
ausgleichsflaechen: 'Compensation area',
5363
hamburgBorder: 'City border Hamburg',
5464
},
5565
},

packages/clients/snowbox/src/mapConfiguration.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import { Feature as GeoJsonFeature } from 'geojson'
2+
import { Feature as OlFeature } from 'ol'
3+
import {
4+
ExtendedMasterportalapiMarkersIsSelectableFunction,
5+
GfiIsSelectableFunction,
6+
} from '@polar/lib-custom-types'
17
import language from './language'
28

39
const eigengrau = '#16161d'
@@ -8,9 +14,40 @@ const basemapId = '23420'
814
const basemapGreyId = '23421'
915
const sBahn = '23050'
1016
const uBahn = '23053'
17+
export const reports = '6059'
18+
const ausgleichsflaechen = '1454'
1119

1220
const hamburgBorder = '6074'
1321

22+
const isAusgleichsflaecheActive = (feature: GeoJsonFeature) =>
23+
new Date(
24+
Date.parse(feature.properties?.vorhaben_zulassung_am.split('.')[2])
25+
).getFullYear() >= 2000
26+
27+
// arbitrary condition for testing
28+
const isEvenId = (mmlid: string) => Number(mmlid.slice(-1)) % 2 === 0
29+
30+
const isReportActive: GfiIsSelectableFunction = (feature) =>
31+
feature.properties?.features
32+
? // client is in cluster mode
33+
feature.properties?.features.reduce(
34+
(accumulator, current) =>
35+
// NOTE: that's how ol/GeoJSON packs clustered features as GeoJSON
36+
isEvenId(current.values_.mmlid) || accumulator,
37+
false
38+
)
39+
: isEvenId(feature.properties?.mmlid)
40+
41+
const isReportSelectable: ExtendedMasterportalapiMarkersIsSelectableFunction = (
42+
feature
43+
) =>
44+
feature
45+
.get('features')
46+
.reduce(
47+
(accumulator, current) => isEvenId(current.get('mmlid')) || accumulator,
48+
false
49+
)
50+
1451
export const mapConfiguration = {
1552
language: 'en',
1653
locales: language,
@@ -26,6 +63,27 @@ export const mapConfiguration = {
2663
},
2764
},
2865
},
66+
extendedMasterportalapiMarkers: {
67+
layers: [reports],
68+
defaultStyle: {
69+
stroke: '#FFFFFF',
70+
fill: '#005CA9',
71+
},
72+
hoverStyle: {
73+
stroke: '#46688E',
74+
fill: '#8BA1B8',
75+
},
76+
selectionStyle: {
77+
stroke: '#FFFFFF',
78+
fill: '#E10019',
79+
},
80+
unselectableStyle: {
81+
stroke: '#FFFFFF',
82+
fill: '#333333',
83+
},
84+
isSelectable: isReportSelectable,
85+
clusterClickZoom: true,
86+
},
2987
addressSearch: {
3088
searchMethods: [
3189
{
@@ -61,6 +119,14 @@ export const mapConfiguration = {
61119
id: sBahn,
62120
title: 'snowbox.attributions.rapid',
63121
},
122+
{
123+
id: reports,
124+
title: 'snowbox.attributions.reports',
125+
},
126+
{
127+
id: ausgleichsflaechen,
128+
title: 'snowbox.attributions.ausgleichsflaechen',
129+
},
64130
],
65131
},
66132
draw: {
@@ -93,6 +159,8 @@ export const mapConfiguration = {
93159
zoomLevel: 9,
94160
},
95161
gfi: {
162+
mode: 'bboxDot',
163+
activeLayerPath: 'plugin/layerChooser/activeMaskIds',
96164
layers: {
97165
[uBahn]: {
98166
geometry: true,
@@ -110,10 +178,24 @@ export const mapConfiguration = {
110178
art: 'Art',
111179
},
112180
},
181+
[reports]: {
182+
geometry: false,
183+
window: true,
184+
// only one of these will be displayed, depending on whether (extended markers && clusters) are on
185+
properties: ['_gfiLayerId', 'mmlid'],
186+
isSelectable: isReportActive,
187+
},
188+
[ausgleichsflaechen]: {
189+
geometry: true,
190+
window: true,
191+
properties: ['vorhaben', 'vorhaben_zulassung_am'],
192+
isSelectable: isAusgleichsflaecheActive,
193+
},
113194
},
114195
coordinateSources: [
115196
'plugin/pins/transformedCoordinate',
116197
'plugin/pins/coordinatesAfterDrag',
198+
'selectedCoordinates',
117199
],
118200
customHighlightStyle: {
119201
stroke: {
@@ -148,6 +230,16 @@ export const mapConfiguration = {
148230
type: 'mask',
149231
name: 'snowbox.layers.rapid',
150232
},
233+
{
234+
id: reports,
235+
type: 'mask',
236+
name: 'snowbox.layers.reports',
237+
},
238+
{
239+
id: ausgleichsflaechen,
240+
type: 'mask',
241+
name: 'snowbox.layers.ausgleichsflaechen',
242+
},
151243
{
152244
id: hamburgBorder,
153245
visibility: true,

packages/clients/snowbox/src/polar-client.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
/* eslint-disable @typescript-eslint/no-non-null-assertion */
22
import polarCore from '@polar/core'
33
import { changeLanguage } from 'i18next'
4+
// NOTE bad pattern, but probably fine for a test client
5+
import { enableClustering } from '../../meldemichel/src/utils/enableClustering'
46
import { addPlugins } from './addPlugins'
5-
import { mapConfiguration } from './mapConfiguration'
7+
import { mapConfiguration, reports } from './mapConfiguration'
68

79
addPlugins(polarCore)
810

@@ -12,7 +14,7 @@ const createMap = (layerConf) => {
1214
containerId: 'polarstern',
1315
mapConfiguration: {
1416
...mapConfiguration,
15-
layerConf,
17+
layerConf: (enableClustering(layerConf, reports), layerConf),
1618
},
1719
})
1820
.then((map) => {

packages/core/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,9 @@ To figure out the name of the locales to override, inspect the matching plugin i
178178
| defaultStyle | MarkerStyle? | Used as the default marker style. The default fill color for these markers is `'#005CA9'`. |
179179
| dispatchOnMapSelect | string[]? | If set, the parameters will be spread to dispatchment on map selection. `['target', 'value']` will `dispatch(...['target', 'value'])`. This can be used to open the iconMenu's GFI with `['plugin/iconMenu/openMenuById', 'gfi']`, should the IconMenu exist and the gfi plugin be in it with this id. |
180180
| hoverStyle | MarkerStyle? | Used as map marker style for hovered features. The default fill color for these markers is `'#7B1045'`. |
181+
| isSelectable | ((feature: GeoJsonFeature) => boolean)? | If undefined, all features are selectable. If defined, this can be used to sort out features to be unselectable, and such features will be styled different and won't react on click. |
181182
| selectionStyle | MarkerStyle? | Used as map marker style for selected features. The default fill color for these markers is `'#679100'`. |
182-
183+
| unselectableStyle | MarkerStyle? | Used as a map marker style for unselectable features. Features are unselectable if a given `isSelectable` method returns falsy for a feature. The default fill color for these markers is `'#333333'`. |
183184

184185
Example configuration:
185186
```js
@@ -197,6 +198,11 @@ extendedMasterportalapiMarkers: {
197198
stroke: '#FFFFFF',
198199
fill: '#E10019',
199200
},
201+
unselectableStyle: {
202+
stroke: '#FFFFFF',
203+
fill: '#333333'
204+
},
205+
isSelectable: (feature: Feature) => feature.get('indicator')
200206
clusterClickZoom: true,
201207
dispatchOnMapSelect: ['plugin/iconMenu/openMenuById', 'gfi'],
202208
},

packages/core/src/utils/markers/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const defaultStrokeWidth = '2'
1515
const defaultFill = '#005CA9'
1616
const defaultHoverFill = '#7B1045'
1717
const defaultSelectionFill = '#679100'
18+
const defaultUnselectableFill = '#333333'
1819

1920
const prefix = 'data:image/svg+xml,'
2021

@@ -124,3 +125,7 @@ export const getHoveredStyle = memoizeStyle(getStyleFunction(defaultHoverFill))
124125
export const getSelectedStyle = memoizeStyle(
125126
getStyleFunction(defaultSelectionFill)
126127
)
128+
129+
export const getUnselectableStyle = memoizeStyle(
130+
getStyleFunction(defaultUnselectableFill)
131+
)

packages/core/src/vuePlugins/actions/useExtendedMasterportalapiMarkers/index.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Feature, MapBrowserEvent } from 'ol'
22
import {
33
CoreGetters,
44
CoreState,
5+
ExtendedMasterportalapiMarkersIsSelectableFunction,
56
MarkerStyle,
67
PolarActionContext,
78
PolarStore,
@@ -11,7 +12,11 @@ import { isVisible } from '@polar/lib-invisible-style'
1112
import VectorLayer from 'ol/layer/Vector'
1213
import BaseLayer from 'ol/layer/Base'
1314
import getCluster from '@polar/lib-get-cluster'
14-
import { getHoveredStyle, getSelectedStyle } from '../../../utils/markers'
15+
import {
16+
getHoveredStyle,
17+
getSelectedStyle,
18+
getUnselectableStyle,
19+
} from '../../../utils/markers'
1520
import { resolveClusterClick } from '../../../utils/resolveClusterClick'
1621
import { setLayerId } from './setLayerId'
1722

@@ -70,12 +75,16 @@ export function useExtendedMasterportalapiMarkers(
7075
{
7176
hoverStyle = {},
7277
selectionStyle = {},
78+
unselectableStyle = {},
79+
isSelectable = () => true,
7380
layers,
7481
clusterClickZoom = false,
7582
dispatchOnMapSelect,
7683
}: {
7784
hoverStyle?: MarkerStyle
7885
selectionStyle?: MarkerStyle
86+
unselectableStyle?: MarkerStyle
87+
isSelectable?: ExtendedMasterportalapiMarkersIsSelectableFunction
7988
layers: string[]
8089
clusterClickZoom: boolean
8190
dispatchOnMapSelect?: string[]
@@ -101,6 +110,20 @@ export function useExtendedMasterportalapiMarkers(
101110
(feature: Feature) =>
102111
isVisible(feature) ? feature.getGeometry() : null
103112
}
113+
const originalStyleFunction = (layer as VectorLayer<Feature>).getStyle()
114+
;(layer as VectorLayer<Feature>).setStyle((feature) => {
115+
if (
116+
typeof isSelectable === 'undefined' ||
117+
isSelectable(feature as Feature)
118+
) {
119+
// @ts-expect-error | always is a function due to masterportalapi design
120+
return originalStyleFunction(feature)
121+
}
122+
return getUnselectableStyle(
123+
unselectableStyle,
124+
feature.get('features').length > 1
125+
)
126+
})
104127
})
105128

106129
// // // STORE EVENT HANDLING
@@ -146,7 +169,7 @@ export function useExtendedMasterportalapiMarkers(
146169
hovered = null
147170
commit('setHovered', hovered)
148171
}
149-
if (!feature) {
172+
if (!feature || !isSelectable(feature)) {
150173
return
151174
}
152175
const isMultiFeature = feature.get('features')?.length > 1
@@ -164,7 +187,11 @@ export function useExtendedMasterportalapiMarkers(
164187
dispatch('updateSelection', { feature: selected })
165188
}
166189
const feature = map.getFeaturesAtPixel(event.pixel, { layerFilter })[0]
167-
if (!feature || feature instanceof RenderFeature) {
190+
if (
191+
!feature ||
192+
feature instanceof RenderFeature ||
193+
!isSelectable(feature)
194+
) {
168195
return
169196
}
170197
const isMultiFeature = feature.get('features')?.length > 1

packages/core/src/vuePlugins/vuex.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,12 @@ export const makeStore = () => {
141141
noop(state.selected)
142142
return selected
143143
},
144+
selectedCoordinates: (state) => {
145+
noop(state.selected)
146+
return selected === null
147+
? null
148+
: (selected.getGeometry() as Point).getCoordinates()
149+
},
144150
// hack: deliver components (outside vuex) based on counter; see NOTE above
145151
components: (state) => {
146152
noop(state.components)

packages/plugins/Gfi/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## unpublished
44

55
- Breaking: Upgrade `@masterportal/masterportalapi` from `2.8.0` to `2.40.0` and subsequently `ol` from `^7.1.0` to `^9.2.4`.
6+
- Feature: Add new configuration parameter `isSelectable` to that can be used to filter features to be unselectable.
67
- Fix: Adjust documentation to properly describe optionality of configuration parameters.
78
- Fix: Add missing configuration parameters `featureList` and `maxFeatures` to the general documentation and `filterBy` and `format` to `gfi.gfiLayerConfiguration`
89
- Refactor: Replace redundant prop-forwarding with `getters`.

packages/plugins/Gfi/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ function afterLoadFunction(featuresByLayerId: Record<string, GeoJsonFeature[]>):
8383
| exportProperty | string? | Property of the features of a service having an url usable to trigger a download of features as a document. |
8484
| geometry | boolean? | If true, feature geometry will be highlighted within the map. Defaults to `false`. |
8585
| geometryName | string? | Name of the geometry property if not the default field. |
86+
| isSelectable | ((feature: GeoJsonFeature) => boolean)? | A function can be defined to allow filtering features to be either selectable (return `true`) or not. Unselectable features will be filtered out by the GFI plugin and have neither GFI display nor store presence, but may be visible in the map nonetheless, depending on your other configuration. Please also mind that usage in combination with `extendedMasterportalapiMarkers` requires further configuration of that feature for smooth UX; see the respective documentation of `@polar/core`. |
8687
| properties | Record<propertyName, displayName>/string[]? | In case `window` is `true`, this will be used to determine which contents to show. In case of an array, keys are used to select properties. In case of an object, keys are used to select properties, but will be titles as their respective values. Displays all properties by default. |
8788
| showTooltip | ((feature: Feature) => [string, string][])? | If given, a tooltip will be shown with the values calculated for the feature. The first string is the HTML tag to render, the second its contents; contants may be locale keys. For more information regarding the strings, see the documentation of the `@polar/lib-tooltip` package. Defaults to `undefined`. Please mind that tooltips will only be shown if a mouse is used or the hovering device could not be detected. Touch and pen interactions do not open tooltips since they will open the GFI window, rendering the gatherable information redundant. |
8889
| window | boolean? | If true, properties will be shown in the map client. Defaults to `false`. |
@@ -106,7 +107,8 @@ layers: {
106107
['div', `Feature ID: ${feature.properties.id}`],
107108
['span', `Coordinates: ${feature.geometry.coordinates.join(', ')}`]
108109
];
109-
};
110+
},
111+
isSelectable: (feature: Feature): boolean => Boolean(Math.random() < 0.5)
110112
},
111113
}
112114
```

packages/plugins/Gfi/src/store/actions/debouncedGfiRequest.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { requestGfi } from '../../utils/requestGfi'
1818
import sortFeatures from '../../utils/sortFeatures'
1919
import { GfiGetters, GfiState } from '../../types'
2020

21-
const mapFeaturesToLayerIds = (
21+
const filterAndMapFeaturesToLayerIds = (
2222
layerKeys: string[],
2323
gfiConfiguration: GfiConfiguration,
2424
features: (symbol | GeoJsonFeature<GeoJsonGeometry, GeoJsonProperties>[])[],
@@ -30,10 +30,12 @@ const mapFeaturesToLayerIds = (
3030
(accumulator, key, index) => ({
3131
...accumulator,
3232
[key]: Array.isArray(features[index])
33-
? (features[index] as []).slice(
34-
0,
35-
gfiConfiguration.layers[key].maxFeatures || generalMaxFeatures
36-
)
33+
? (features[index] as [])
34+
.filter(gfiConfiguration.layers[key].isSelectable || (() => true))
35+
.slice(
36+
0,
37+
gfiConfiguration.layers[key].maxFeatures || generalMaxFeatures
38+
)
3739
: features[index],
3840
}),
3941
{}
@@ -135,7 +137,7 @@ const gfiRequest =
135137
: errorSymbol(result.reason.message)
136138
)
137139
const srsName: string = map.getView().getProjection().getCode()
138-
let featuresByLayerId = mapFeaturesToLayerIds(
140+
let featuresByLayerId = filterAndMapFeaturesToLayerIds(
139141
layerKeys,
140142
// NOTE if there was no configuration, we would not be here
141143
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion

0 commit comments

Comments
 (0)