Skip to content

Commit

Permalink
Merge pull request #197 from Dataport/feature/unselectable-features
Browse files Browse the repository at this point in the history
implement unselectable features
  • Loading branch information
dopenguin authored Nov 5, 2024
2 parents 60f452f + fd6ec89 commit 8b112a6
Show file tree
Hide file tree
Showing 14 changed files with 191 additions and 15 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
91 changes: 91 additions & 0 deletions packages/clients/snowbox/src/mapConfiguration.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import { Feature as GeoJsonFeature } from 'geojson'
import {
ExtendedMasterportalapiMarkersIsSelectableFunction,
GfiIsSelectableFunction,
} from '@polar/lib-custom-types'
import language from './language'

const eigengrau = '#16161d'
Expand All @@ -8,9 +13,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 +62,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 +118,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 +158,8 @@ export const mapConfiguration = {
zoomLevel: 9,
},
gfi: {
mode: 'bboxDot',
activeLayerPath: 'plugin/layerChooser/activeMaskIds',
layers: {
[uBahn]: {
geometry: true,
Expand All @@ -110,10 +177,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 +229,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
1 change: 1 addition & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- Breaking: Upgrade `@masterportal/masterportalapi` from `2.8.0` to `2.40.0` and subsequently `ol` from `^7.1.0` to `^9.2.4`.
- Breaking: Remove support for marking client CSS via `data-polar="true"`. Please use the configuration parameter `stylePath` instead.
- Feature: The `extendedMasterportalapiFeatures` feature has been extended by a `isSelectable` function and `unselectableStyle` to style markers accordingly.
- Feature: Add new state parameter `mapHasDimensions` to let plugins have a "hook" to react on when the map is ready.
- Feature: Add `deviceIsHorizontal` as a getter to have a more central place to check if the device is in landscape mode.
- Feature: Add clearer documentation regarding `@masterportal/masterportalapi` related configuration parameters including examples.
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` 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
Loading

0 comments on commit 8b112a6

Please sign in to comment.