Skip to content

Commit

Permalink
implement unselectable features
Browse files Browse the repository at this point in the history
  • Loading branch information
warm-coolguy committed Oct 8, 2024
1 parent 465a24d commit 6ff71ad
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 13 deletions.
10 changes: 10 additions & 0 deletions packages/clients/snowbox/src/language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,18 @@ const language: LanguageOption[] = [
'Strecken U-Bahn © Freie und Hansestadt Hamburg, Behörde für Wirtschaft, Verkehr und Innovation',
rapid:
'Strecken S-Bahn © Freie und Hansestadt Hamburg, Behörde für Wirtschaft, Verkehr und Innovation',
reports: 'Meldungen durch Bürger',
ausgleichsflaechen:
'Ausgleichsflächen © Freie und Hansestadt Hamburg, Behörde für Umwelt und Energie',
hamburgBorder: 'Landesgrenze Hamburg © Freie und Hansestadt Hamburg',
},
layers: {
basemap: 'Basemap.de (Farbe)',
basemapGrey: 'Basemap.de (Grau)',
underground: 'U-Bahn',
rapid: 'S-Bahn',
reports: 'Anliegen (MML)',
ausgleichsflaechen: 'Ausgleichsflächen',
hamburgBorder: 'Landesgrenze Hamburg',
},
},
Expand All @@ -43,13 +48,18 @@ const language: LanguageOption[] = [
'Railway Lines U-Bahn © Freie und Hansestadt Hamburg, Behörde für Wirtschaft, Verkehr und Innovation',
rapid:
'Railway Lines S-Bahn © Freie und Hansestadt Hamburg, Behörde für Wirtschaft, Verkehr und Innovation',
reports: 'Reports by citizens',
ausgleichsflaechen:
'Compensation area © Freie und Hansestadt Hamburg, Behörde für Umwelt und Energie',
hamburgBorder: 'City border Hamburg © Freie und Hansestadt Hamburg',
},
layers: {
basemap: 'Basemap.de (Coloured)',
basemapGrey: 'Basemap.de (Grey)',
underground: 'Underground railway (U-Bahn)',
rapid: 'City rapid railway (S-Bahn)',
reports: 'Reports (MML)',
ausgleichsflaechen: 'Compensation area',
hamburgBorder: 'City border Hamburg',
},
},
Expand Down
92 changes: 92 additions & 0 deletions packages/clients/snowbox/src/mapConfiguration.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import { Feature as GeoJsonFeature } from 'geojson'
import { Feature as OlFeature } from 'ol'

Check failure on line 2 in packages/clients/snowbox/src/mapConfiguration.ts

View workflow job for this annotation

GitHub Actions / lint

'OlFeature' is defined but never used

Check warning on line 2 in packages/clients/snowbox/src/mapConfiguration.ts

View workflow job for this annotation

GitHub Actions / lint

'OlFeature' is defined but never used
import {
ExtendedMasterportalapiMarkersIsSelectableFunction,
GfiIsSelectableFunction,
} from '@polar/lib-custom-types'
import language from './language'

const eigengrau = '#16161d'
Expand All @@ -8,9 +14,40 @@ const basemapId = '23420'
const basemapGreyId = '23421'
const sBahn = '23050'
const uBahn = '23053'
export const reports = '6059'
const ausgleichsflaechen = '1454'

const hamburgBorder = '6074'

const isAusgleichsflaecheActive = (feature: GeoJsonFeature) =>
new Date(
Date.parse(feature.properties?.vorhaben_zulassung_am.split('.')[2])
).getFullYear() >= 2000

// arbitrary condition for testing
const isEvenId = (mmlid: string) => Number(mmlid.slice(-1)) % 2 === 0

const isReportActive: GfiIsSelectableFunction = (feature) =>
feature.properties?.features
? // client is in cluster mode
feature.properties?.features.reduce(
(accumulator, current) =>
// NOTE: that's how ol/GeoJSON packs clustered features as GeoJSON
isEvenId(current.values_.mmlid) || accumulator,
false
)
: isEvenId(feature.properties?.mmlid)

const isReportSelectable: ExtendedMasterportalapiMarkersIsSelectableFunction = (
feature
) =>
feature
.get('features')
.reduce(
(accumulator, current) => isEvenId(current.get('mmlid')) || accumulator,
false
)

export const mapConfiguration = {
language: 'en',
locales: language,
Expand All @@ -26,6 +63,27 @@ export const mapConfiguration = {
},
},
},
extendedMasterportalapiMarkers: {
layers: [reports],
defaultStyle: {
stroke: '#FFFFFF',
fill: '#005CA9',
},
hoverStyle: {
stroke: '#46688E',
fill: '#8BA1B8',
},
selectionStyle: {
stroke: '#FFFFFF',
fill: '#E10019',
},
unselectableStyle: {
stroke: '#FFFFFF',
fill: '#333333',
},
isSelectable: isReportSelectable,
clusterClickZoom: true,
},
addressSearch: {
searchMethods: [
{
Expand Down Expand Up @@ -61,6 +119,14 @@ export const mapConfiguration = {
id: sBahn,
title: 'snowbox.attributions.rapid',
},
{
id: reports,
title: 'snowbox.attributions.reports',
},
{
id: ausgleichsflaechen,
title: 'snowbox.attributions.ausgleichsflaechen',
},
],
},
draw: {
Expand Down Expand Up @@ -93,6 +159,8 @@ export const mapConfiguration = {
zoomLevel: 9,
},
gfi: {
mode: 'bboxDot',
activeLayerPath: 'plugin/layerChooser/activeMaskIds',
layers: {
[uBahn]: {
geometry: true,
Expand All @@ -110,10 +178,24 @@ export const mapConfiguration = {
art: 'Art',
},
},
[reports]: {
geometry: false,
window: true,
// only one of these will be displayed, depending on whether (extended markers && clusters) are on
properties: ['_gfiLayerId', 'mmlid'],
isSelectable: isReportActive,
},
[ausgleichsflaechen]: {
geometry: true,
window: true,
properties: ['vorhaben', 'vorhaben_zulassung_am'],
isSelectable: isAusgleichsflaecheActive,
},
},
coordinateSources: [
'plugin/pins/transformedCoordinate',
'plugin/pins/coordinatesAfterDrag',
'selectedCoordinates',
],
customHighlightStyle: {
stroke: {
Expand Down Expand Up @@ -148,6 +230,16 @@ export const mapConfiguration = {
type: 'mask',
name: 'snowbox.layers.rapid',
},
{
id: reports,
type: 'mask',
name: 'snowbox.layers.reports',
},
{
id: ausgleichsflaechen,
type: 'mask',
name: 'snowbox.layers.ausgleichsflaechen',
},
{
id: hamburgBorder,
visibility: true,
Expand Down
6 changes: 4 additions & 2 deletions packages/clients/snowbox/src/polar-client.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import polarCore from '@polar/core'
import { changeLanguage } from 'i18next'
// NOTE bad pattern, but probably fine for a test client
import { enableClustering } from '../../meldemichel/src/utils/enableClustering'
import { addPlugins } from './addPlugins'
import { mapConfiguration } from './mapConfiguration'
import { mapConfiguration, reports } from './mapConfiguration'

addPlugins(polarCore)

Expand All @@ -12,7 +14,7 @@ const createMap = (layerConf) => {
containerId: 'polarstern',
mapConfiguration: {
...mapConfiguration,
layerConf,
layerConf: (enableClustering(layerConf, reports), layerConf),
},
})
.then((map) => {
Expand Down
8 changes: 7 additions & 1 deletion packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,9 @@ To figure out the name of the locales to override, inspect the matching plugin i
| defaultStyle | MarkerStyle? | Used as the default marker style. The default fill color for these markers is `'#005CA9'`. |
| 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. |
| hoverStyle | MarkerStyle? | Used as map marker style for hovered features. The default fill color for these markers is `'#7B1045'`. |
| 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. |
| selectionStyle | MarkerStyle? | Used as map marker style for selected features. The default fill color for these markers is `'#679100'`. |

| 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'`. |

Example configuration:
```js
Expand All @@ -197,6 +198,11 @@ extendedMasterportalapiMarkers: {
stroke: '#FFFFFF',
fill: '#E10019',
},
unselectableStyle: {
stroke: '#FFFFFF',
fill: '#333333'
},
isSelectable: (feature: Feature) => feature.get('indicator')
clusterClickZoom: true,
dispatchOnMapSelect: ['plugin/iconMenu/openMenuById', 'gfi'],
},
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/utils/markers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const defaultStrokeWidth = '2'
const defaultFill = '#005CA9'
const defaultHoverFill = '#7B1045'
const defaultSelectionFill = '#679100'
const defaultUnselectableFill = '#333333'

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

Expand Down Expand Up @@ -124,3 +125,7 @@ export const getHoveredStyle = memoizeStyle(getStyleFunction(defaultHoverFill))
export const getSelectedStyle = memoizeStyle(
getStyleFunction(defaultSelectionFill)
)

export const getUnselectableStyle = memoizeStyle(
getStyleFunction(defaultUnselectableFill)
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Feature, MapBrowserEvent } from 'ol'
import {
CoreGetters,
CoreState,
ExtendedMasterportalapiMarkersIsSelectableFunction,
MarkerStyle,
PolarActionContext,
PolarStore,
Expand All @@ -11,7 +12,11 @@ import { isVisible } from '@polar/lib-invisible-style'
import VectorLayer from 'ol/layer/Vector'
import BaseLayer from 'ol/layer/Base'
import getCluster from '@polar/lib-get-cluster'
import { getHoveredStyle, getSelectedStyle } from '../../../utils/markers'
import {
getHoveredStyle,
getSelectedStyle,
getUnselectableStyle,
} from '../../../utils/markers'
import { resolveClusterClick } from '../../../utils/resolveClusterClick'
import { setLayerId } from './setLayerId'

Expand Down Expand Up @@ -70,12 +75,16 @@ export function useExtendedMasterportalapiMarkers(
{
hoverStyle = {},
selectionStyle = {},
unselectableStyle = {},
isSelectable = () => true,
layers,
clusterClickZoom = false,
dispatchOnMapSelect,
}: {
hoverStyle?: MarkerStyle
selectionStyle?: MarkerStyle
unselectableStyle?: MarkerStyle
isSelectable?: ExtendedMasterportalapiMarkersIsSelectableFunction
layers: string[]
clusterClickZoom: boolean
dispatchOnMapSelect?: string[]
Expand All @@ -101,6 +110,20 @@ export function useExtendedMasterportalapiMarkers(
(feature: Feature) =>
isVisible(feature) ? feature.getGeometry() : null
}
const originalStyleFunction = (layer as VectorLayer<Feature>).getStyle()
;(layer as VectorLayer<Feature>).setStyle((feature) => {
if (
typeof isSelectable === 'undefined' ||
isSelectable(feature as Feature)
) {
// @ts-expect-error | always is a function due to masterportalapi design
return originalStyleFunction(feature)
}
return getUnselectableStyle(
unselectableStyle,
feature.get('features').length > 1
)
})
})

// // // STORE EVENT HANDLING
Expand Down Expand Up @@ -146,7 +169,7 @@ export function useExtendedMasterportalapiMarkers(
hovered = null
commit('setHovered', hovered)
}
if (!feature) {
if (!feature || !isSelectable(feature)) {
return
}
const isMultiFeature = feature.get('features')?.length > 1
Expand All @@ -164,7 +187,11 @@ export function useExtendedMasterportalapiMarkers(
dispatch('updateSelection', { feature: selected })
}
const feature = map.getFeaturesAtPixel(event.pixel, { layerFilter })[0]
if (!feature || feature instanceof RenderFeature) {
if (
!feature ||
feature instanceof RenderFeature ||
!isSelectable(feature)
) {
return
}
const isMultiFeature = feature.get('features')?.length > 1
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/vuePlugins/vuex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ export const makeStore = () => {
noop(state.selected)
return selected
},
selectedCoordinates: (state) => {
noop(state.selected)
return selected === null
? null
: (selected.getGeometry() as Point).getCoordinates()
},
// hack: deliver components (outside vuex) based on counter; see NOTE above
components: (state) => {
noop(state.components)
Expand Down
1 change: 1 addition & 0 deletions packages/plugins/Gfi/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## unpublished

- Breaking: Upgrade `@masterportal/masterportalapi` from `2.8.0` to `2.40.0` and subsequently `ol` from `^7.1.0` to `^9.2.4`.
- Feature: Add new configuration parameter `isSelectable` to that can be used to filter features to be unselectable.
- Fix: Adjust documentation to properly describe optionality of configuration parameters.
- Fix: Add missing configuration parameters `featureList` and `maxFeatures` to the general documentation and `filterBy` and `format` to `gfi.gfiLayerConfiguration`
- Refactor: Replace redundant prop-forwarding with `getters`.
Expand Down
4 changes: 3 additions & 1 deletion packages/plugins/Gfi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ function afterLoadFunction(featuresByLayerId: Record<string, GeoJsonFeature[]>):
| exportProperty | string? | Property of the features of a service having an url usable to trigger a download of features as a document. |
| geometry | boolean? | If true, feature geometry will be highlighted within the map. Defaults to `false`. |
| geometryName | string? | Name of the geometry property if not the default field. |
| 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`. |
| 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. |
| 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. |
| window | boolean? | If true, properties will be shown in the map client. Defaults to `false`. |
Expand All @@ -106,7 +107,8 @@ layers: {
['div', `Feature ID: ${feature.properties.id}`],
['span', `Coordinates: ${feature.geometry.coordinates.join(', ')}`]
];
};
},
isSelectable: (feature: Feature): boolean => Boolean(Math.random() < 0.5)
},
}
```
Expand Down
14 changes: 8 additions & 6 deletions packages/plugins/Gfi/src/store/actions/debouncedGfiRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { requestGfi } from '../../utils/requestGfi'
import sortFeatures from '../../utils/sortFeatures'
import { GfiGetters, GfiState } from '../../types'

const mapFeaturesToLayerIds = (
const filterAndMapFeaturesToLayerIds = (
layerKeys: string[],
gfiConfiguration: GfiConfiguration,
features: (symbol | GeoJsonFeature<GeoJsonGeometry, GeoJsonProperties>[])[],
Expand All @@ -30,10 +30,12 @@ const mapFeaturesToLayerIds = (
(accumulator, key, index) => ({
...accumulator,
[key]: Array.isArray(features[index])
? (features[index] as []).slice(
0,
gfiConfiguration.layers[key].maxFeatures || generalMaxFeatures
)
? (features[index] as [])
.filter(gfiConfiguration.layers[key].isSelectable || (() => true))
.slice(
0,
gfiConfiguration.layers[key].maxFeatures || generalMaxFeatures
)
: features[index],
}),
{}
Expand Down Expand Up @@ -135,7 +137,7 @@ const gfiRequest =
: errorSymbol(result.reason.message)
)
const srsName: string = map.getView().getProjection().getCode()
let featuresByLayerId = mapFeaturesToLayerIds(
let featuresByLayerId = filterAndMapFeaturesToLayerIds(
layerKeys,
// NOTE if there was no configuration, we would not be here
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
Expand Down
Loading

0 comments on commit 6ff71ad

Please sign in to comment.