-
Notifications
You must be signed in to change notification settings - Fork 8
Refactor/display feature count on cluster markers #932
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: next
Are you sure you want to change the base?
Changes from all commits
0dae3e4
8d37be1
9ca13ac
b2a94e9
7d625f4
cc455bf
e76c70c
4c024b7
26af65b
5bb8e81
eb9ee19
09ab963
9df5176
d2433d5
012d72d
fd31cf0
746f668
6c6c39d
06cce03
6272014
a02d79d
0a145ff
d6d1d8a
ff3b000
68774b9
0901e35
b50a005
be122e0
0c0c07f
7728138
41ace1a
21aaa9a
8c119b7
0c69b4e
daae70e
a59a709
a44def9
f8acef8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,19 @@ | ||
| import type { Feature } from 'ol' | ||
| import type { Style } from 'ol/style' | ||
|
|
||
| export type MarkersIsSelectableFunction = (feature: Feature) => boolean | ||
|
|
||
| export type GetMarkerFunction = ( | ||
| style: MarkerStyle, | ||
| count: number, | ||
| displayFeatureCount: boolean | ||
| ) => Style | ||
| export type GetSVGConfigFunction = (digits: string) => MarkerSVGConfig | ||
| export type GetTextPositionFunction = (path: string) => TextPosition | ||
| export type PinShape = 'circle' | 'pill' | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As this is not used anywhere else, this can just directly be set on Also, do you think there will be a third option? Otherwise, using a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes I thought, maybe there will be more different markers, like rectangle ones or square ones.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see no need to change this, any change would be arbitrary. 🎩
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The type is still superfluous but may stay with the options |
||
| export interface TextPosition { | ||
| x: number | ||
| y: number | ||
| } | ||
| export interface CallOnMapSelect { | ||
| action: string | ||
| payload: unknown | ||
|
|
@@ -33,7 +45,7 @@ export interface MarkerStyle { | |
| /** | ||
| * `width` and `height` of the `<svg>`-cluster-marker. | ||
| * | ||
| * @defaultValue `[40, 36]` | ||
| * @defaultValue `[40 * 2, 36 * 2]` | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not 80 and 72? If changed, this should be done on both types and all other instances.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because 40 and 36 were the smallest values that worked well, I thought it would make sense to keep them and scale them up.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense to me. Please add that reason to the comment so that the next person reading knows; it's a nice information to have for future modification/configurability. |
||
| */ | ||
| clusterSize: [number, number] | ||
|
|
||
|
|
@@ -45,7 +57,7 @@ export interface MarkerStyle { | |
| /** | ||
| * `width` and `height` of the `<svg>`-marker. | ||
| * | ||
| * @defaultValue `[26, 36]` | ||
| * @defaultValue `[40 * 2, 36 * 2]` | ||
| */ | ||
| size: [number, number] | ||
|
|
||
|
|
@@ -62,6 +74,74 @@ export interface MarkerStyle { | |
| * @defaultValue `'2'` | ||
| */ | ||
| strokeWidth: string | number | ||
|
|
||
| /** | ||
| * Text to display on the marker. | ||
| * | ||
| */ | ||
| clusterCount?: number | ||
| } | ||
|
|
||
| /** | ||
| * The MarkerSVGConfig contains the SVG-informations needed to create the svg for the marker. | ||
| */ | ||
| export interface MarkerSVGConfig { | ||
| /** | ||
| * The SVG path for the markershape where the text is displayed in. | ||
| */ | ||
| contentPath: string | ||
|
|
||
| /** | ||
| * The definitions for the marker (e.g. shadow patterns with image data). | ||
| */ | ||
| defs: string | ||
|
|
||
| /** | ||
| * Calculates the x and y coordinates for the text position within the marker. | ||
| * The calculation is based on the provided SVG path | ||
| * @throws Error If the provided path does not match the expected marker pattern. | ||
| * @returns The x and y coordinates for the text position within the marker. | ||
| */ | ||
| getTextPosition: GetTextPositionFunction | ||
|
|
||
| /** | ||
| * The shape of the marker. | ||
| */ | ||
| pinShape: PinShape | ||
|
|
||
| /** | ||
| * The shadow path for the main/front marker layer. | ||
| */ | ||
| shadowPath: string | ||
|
|
||
| /** The outer shape of the marker. */ | ||
| shapePath: string | ||
|
|
||
| /** | ||
| * The SVG shadow paths for the stacked marker layers. | ||
| */ | ||
|
Comment on lines
+120
to
+122
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment (just like later for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would be true. The implementation needs every string to be accessable at it's own. In order to build the svg in marker.ts it is neseccary to place them in the correct order. The implementation now also allows to "read" what is happening in lines 51-84.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's plausible enough to just copy-paste the comment to each field singularized so that tsdoc can correctly document it. Should you not like that, I furthermore have this suggestion: proposal.patch It does add a practically unneeded type, but I find synonyms to be nice for readability and deduplication. Either solution would be fine with me. Or maybe you actually want to go |
||
| stackedShadow1: string | ||
| stackedShadow2: string | ||
|
|
||
| /** | ||
| * The SVG paths for the stacked shape of the marker. | ||
| */ | ||
| stackedShape1: string | ||
| stackedShape2: string | ||
| stackedTip1: string | ||
| stackedTip2: string | ||
|
|
||
| readonly textPosition: TextPosition | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How come this is the only
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The readonly property allows for more relaxed access to the text position later: svgConfig.textPosition. And even if the calculation for each svgConfig is different, or even a different function is behind it, the call remains reliably the same.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've removed the |
||
|
|
||
| /** | ||
| * The SVG path for the tip of the marker. | ||
| */ | ||
| tipPath: string | ||
|
|
||
| /** | ||
| * The viewBox for the marker. | ||
| */ | ||
| viewBox: string | ||
| } | ||
|
|
||
| export interface MarkerLayer { | ||
|
|
@@ -150,4 +230,10 @@ export interface MarkerConfiguration { | |
| * take place. Defaults to `false`. | ||
| */ | ||
| clusterClickZoom?: boolean | ||
|
|
||
| /** | ||
| * If `true`, the number of features in a cluster will be displayed on the cluster marker. | ||
| * @defaultValue `false` | ||
| */ | ||
| displayFeatureCount?: boolean | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,12 @@ | ||
| import type { Feature, Map, MapBrowserEvent, MapEvent } from 'ol' | ||
| import type BaseLayer from 'ol/layer/Base' | ||
| import type VectorSource from 'ol/source/Vector' | ||
| import type { MarkerLayer, MarkerStyle, PluginId } from '../../types' | ||
| import type { | ||
| GetMarkerFunction, | ||
| MarkerLayer, | ||
| MarkerStyle, | ||
| PluginId, | ||
| } from '../../types' | ||
|
|
||
| import { toMerged } from 'es-toolkit' | ||
| import { createEmpty, extend } from 'ol/extent' | ||
|
|
@@ -16,13 +21,14 @@ import { isVisible } from '@/lib/invisibleStyle' | |
| import { useMainStore } from '../../stores/main' | ||
| import { useMarkerStore } from '../../stores/marker' | ||
| import { usePluginStore } from '../../stores/plugin' | ||
| import { getMarkerStyle } from '../markers' | ||
| import { createGetMarkerStyle } from '../markers' | ||
|
|
||
| let stopWatcher: (() => void) | null = null | ||
| let getMarkerStyle: GetMarkerFunction = createGetMarkerStyle(() => {}) | ||
|
|
||
| // these have been measured to fit once and influence marker size | ||
| const imgSize: [number, number] = [26, 36] | ||
| const imgSizeMulti: [number, number] = [40, 36] | ||
| const imgSize: [number, number] = [40 * 2, 36 * 2] | ||
| const imgSizeMulti: [number, number] = [40 * 2, 36 * 2] | ||
|
|
||
| const defaultStroke = '#FFFFFF' | ||
| const defaultStrokeWidth = '2' | ||
|
|
@@ -110,12 +116,12 @@ function updateSelection( | |
| typeof findLayer(map, layerId)?.getSource().getDistance === 'function' | ||
| ? getCluster(map, feature, '_polarLayerId') | ||
| : feature | ||
|
|
||
| selectedCluster.setStyle( | ||
| getMarkerStyle( | ||
| getLayerConfiguration(feature.get('_polarLayerId') as string) | ||
| .selectionStyle, | ||
| selectedCluster.get('features')?.length > 1 | ||
| selectedCluster.get('features')?.length, | ||
| store.displayFeatureCount | ||
| ) | ||
| ) | ||
|
|
||
|
|
@@ -162,6 +168,10 @@ export function setupMarkers(map: Map) { | |
| return | ||
| } | ||
|
|
||
| getMarkerStyle = createGetMarkerStyle(() => { | ||
| map.render() | ||
| }) | ||
|
|
||
| layers = configuration.layers.map((layer) => | ||
| toMerged( | ||
| { | ||
|
|
@@ -199,7 +209,8 @@ export function setupMarkers(map: Map) { | |
| layerConfiguration.isSelectable(feature as Feature) | ||
| ? layerConfiguration.defaultStyle | ||
| : layerConfiguration.unselectableStyle, | ||
| feature.get('features')?.length > 1 | ||
| feature.get('features')?.length, | ||
| store.displayFeatureCount | ||
| ) | ||
| ) | ||
| }) | ||
|
|
@@ -208,20 +219,19 @@ export function setupMarkers(map: Map) { | |
|
|
||
| stopWatcher = watch( | ||
| () => store.hovered, | ||
| (feature) => { | ||
| if (feature !== null && feature !== toRaw(store.selected)) { | ||
| store.hovered?.setStyle(undefined) | ||
| store.hovered = null | ||
| (feature, oldFeature) => { | ||
| if (oldFeature !== null && oldFeature !== toRaw(store.selected)) { | ||
| oldFeature.setStyle(undefined) | ||
| } | ||
| if (feature !== null && feature !== toRaw(store.selected)) { | ||
| store.hovered = markRaw(feature) | ||
| const isMultiFeature = store.hovered.get('features')?.length > 1 | ||
| const featureCount = feature.get('features')?.length | ||
| const style = getMarkerStyle( | ||
| getLayerConfiguration(feature.get('_polarLayerId') as string) | ||
| .hoverStyle, | ||
| isMultiFeature | ||
| featureCount, | ||
| store.displayFeatureCount | ||
| ) | ||
| store.hovered.setStyle(style) | ||
| feature.setStyle(style) | ||
|
warm-coolguy marked this conversation as resolved.
|
||
| } | ||
| } | ||
| ) | ||
|
|
@@ -242,6 +252,7 @@ export function teardownMarkers(map: Map) { | |
| stopWatcher = null | ||
| layers = [] | ||
| lastClickEvent = null | ||
| getMarkerStyle = createGetMarkerStyle(() => {}) | ||
|
|
||
| map.un('moveend', mapMoveEnd) | ||
| map.un('pointermove', mapPointerMove) | ||
|
|
@@ -274,31 +285,25 @@ function mapPointerMove({ map, pixel }: MapBrowserEvent) { | |
| layerFilter, | ||
| })[0] | ||
|
|
||
| if (feature === toRaw(store.selected) || feature instanceof RenderFeature) { | ||
| if (feature === toRaw(store.hovered)) { | ||
| return | ||
| } | ||
| if ( | ||
| toRaw(store.hovered) !== null && | ||
| toRaw(store.hovered) !== toRaw(store.selected) | ||
| ) { | ||
| store.hovered?.setStyle(undefined) | ||
| store.hovered = null | ||
| } | ||
|
|
||
| if (feature === toRaw(store.selected) || feature instanceof RenderFeature) { | ||
| return | ||
| } | ||
| if (!feature) { | ||
| store.hovered = null | ||
| return | ||
| } | ||
| setLayerId(map, feature) | ||
| const layerConfiguration = getLayerConfiguration( | ||
| feature.get('_polarLayerId') as string | ||
| ) | ||
| if (!layerConfiguration.isSelectable(feature)) { | ||
| store.hovered = null | ||
| return | ||
|
Comment on lines
-271
to
290
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please elaborate
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is implemented to help the performance while one is hovering over the map. First it is checked that the selected feature keeps it stlye when hovered over. Then it is checked whether there was a different marker hovered over which is not selected. In that case the hover-style will be removed and the marker will regain its normal style. This should prevent, that an old hover-style can linger in the store.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would like to propose this reordering: If we're checking first whether feature is falsy, the follow-up checks can be skipped. The gains are minimal, but since the mouse is moved a lot, maybe this will make a difference in a heavy-load scenario? Might also be purely theoretical ... hence, 🎩 on this.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it is a good idea to check whether this approach works, @warm-coolguy |
||
| } | ||
| const isMultiFeature = feature.get('features')?.length > 1 | ||
| feature.setStyle( | ||
| getMarkerStyle(layerConfiguration.hoverStyle, isMultiFeature) | ||
| ) | ||
| store.hovered = markRaw(feature) | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type is redundant as the information can be inferred.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typing ensures that this function always returns an MarkerSVGConfig-object. Should the function typing be completely removed? Or do you have something different in mind?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dopenguin likes them completely removed while they're inferrable. I personally disagree with the same argument you provided: It will, on later changes, make sure that there's no accidental type change due to an oversight.
IMO you may decide either way. If you keep it, you may just resolve this conversation. 🎩
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I proclaim YAGNI