Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
0dae3e4
refactor: make marker feature count display configurable from index.js
dMapybara Aug 11, 2026
8d37be1
refactor: correction for linting, unecessary blank line
dMapybara Aug 11, 2026
9ca13ac
refactor: add marker SVG config and text position types
dMapybara Aug 11, 2026
b2a94e9
refactor: add functions to calculate the center for the textposition
dMapybara Aug 11, 2026
7d625f4
refactor: add svgConfig for 1-5 digits
dMapybara Aug 11, 2026
cc455bf
refactor: add a function to get svgConfig
dMapybara Aug 11, 2026
e76c70c
refactor: introduce the singleSVGMarker to the makeMarker-function
dMapybara Aug 11, 2026
4c024b7
refactor: use the svgConfig in the makeMultiMarker-function
dMapybara Aug 11, 2026
26af65b
refactor: add textelement to the marker
dMapybara Aug 11, 2026
5bb8e81
refactor: extract warning for MeoLeak into separate function
dMapybara Aug 11, 2026
eb9ee19
refactor: memoStyle-function is now using count and displayFeatureCount
dMapybara Aug 11, 2026
09ab963
refactor: add memoCountStyle-function
dMapybara Aug 11, 2026
9df5176
refactor: split style memoization-function by displayFeatureCount
dMapybara Aug 11, 2026
d2433d5
refactor: update the getStyleFunction
dMapybara Aug 11, 2026
012d72d
refactor: use more complex pins
dMapybara Aug 13, 2026
fd31cf0
refactor: adjust skale
dMapybara Aug 14, 2026
746f668
refactor: improve perfomance
dMapybara Aug 14, 2026
6c6c39d
fix: prevent redundant hover style updates in mapPointerMove and remo…
dMapybara Aug 14, 2026
06cce03
refactor: set displayFeatureCount ture
dMapybara Aug 17, 2026
6272014
refactor: adjust font and correct clustermarker size
dMapybara Aug 17, 2026
a02d79d
refactor: correct size of marker and font
dMapybara Aug 17, 2026
0a145ff
refactor: use new magic numbers to make the markers bigger
dMapybara Aug 17, 2026
d6d1d8a
fix: withou the defs in the svg - the markes will not render probably…
dMapybara Aug 18, 2026
ff3b000
fix: implement baked shadows to increase the performance
dMapybara Aug 18, 2026
68774b9
Merge branch 'next' into refactor/display-feature-count-on-cluster-ma…
dMapybara Aug 18, 2026
0901e35
Merge branch 'refactor/improve-marker-size' into refactor/display-fea…
dMapybara Aug 18, 2026
b50a005
fix: removed deprecated function
dMapybara Aug 20, 2026
be122e0
fix: give multimarker with 1-digit a stacked shadow
dMapybara Aug 20, 2026
0c0c07f
fix: use faster encoding fct to reduce lags while zooming in and out
dMapybara Aug 21, 2026
7728138
fix: adjust the marker size to be the same size
dMapybara Aug 21, 2026
41ace1a
fix: import correct shape
dMapybara Aug 21, 2026
21aaa9a
refactor(core): drop redundant cast
dMapybara Aug 24, 2026
8c119b7
perf(core): rasterize cluster marker icons once to fix zoom/hover lag
dopenguin Aug 24, 2026
0c69b4e
style: apply suggestions to make code nicer
dMapybara Sep 4, 2026
daae70e
fix: add hyphen
dMapybara Sep 4, 2026
a59a709
style: apply suggestions to make code nicer
dMapybara Sep 4, 2026
a44def9
style: apply suggestions to make code nicer
dMapybara Sep 4, 2026
f8acef8
style: apply suggestions to make code nicer
dMapybara Sep 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/snowbox/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
colorScheme,
startCenter: [565874, 5934140],
layers: [
// TODO: Add internalization to snowbox

Check warning on line 97 in examples/snowbox/index.js

View workflow job for this annotation

GitHub Actions / Linting

Unexpected 'todo' comment: 'TODO: Add internalization to snowbox'
{
id: basemapId,
visibility: true,
Expand Down Expand Up @@ -178,6 +178,7 @@
},
],
clusterClickZoom: true,
displayFeatureCount: true,
},
// theme: dataportTheme,
locales: [
Expand Down
5 changes: 5 additions & 0 deletions src/core/stores/marker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export const useMarkerStore = defineStore('marker', () => {
() => (configuration.value?.clusterClickZoom as boolean) || false
)

const displayFeatureCount = computed(
() => configuration.value?.displayFeatureCount || false
)

const hovered = shallowRef<Feature | null>(null)
const selected = shallowRef<Feature | null>(null)
const selectedCoordinates = computed(() =>
Expand All @@ -32,6 +36,7 @@ export const useMarkerStore = defineStore('marker', () => {
configuration,
callOnMapSelect,
clusterClickZoom,
displayFeatureCount,

hovered,
selected,
Expand Down
92 changes: 89 additions & 3 deletions src/core/types/marker.ts
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

Copy link
Copy Markdown
Member

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.

@dMapybara dMapybara Aug 24, 2026 •

Copy link
Copy Markdown
Contributor Author

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?

Copy link
Copy Markdown
Member

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. 🎩

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I proclaim YAGNI

export type GetTextPositionFunction = (path: string) => TextPosition
export type PinShape = 'circle' | 'pill'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 MarkerSVGConfig.

Also, do you think there will be a third option? Otherwise, using a boolean with a different name seems easier.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see no need to change this, any change would be arbitrary. 🎩

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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
Expand Down Expand Up @@ -33,7 +45,7 @@ export interface MarkerStyle {
/**
* `width` and `height` of the `<svg>`-cluster-marker.
*
* @defaultValue `[40, 36]`
* @defaultValue `[40 * 2, 36 * 2]`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.

@warm-coolguy warm-coolguy Sep 9, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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]

Expand All @@ -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]

Expand All @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment (just like later for stackedShape1) only belongs to stackedShadow1.
This could be fixed with the object signature being different e.g. with stackedShadow: [string, string]; the same would apply for stackedShape and stackedTip

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.
We would lose this readability in favor to correct that comment.
And I could imagine that it could cause confusion in the future.
There would be an array and it has to have 2 members, even when there is no stacked shadow like:
stackedShadow = ['', '']
On the other hand, this would make the svgConfig a bit leaner.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 ['', ''] or find yet another way to solve this. The choice is yours!

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come this is the only readonly property? Is this relevan in some way?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed the readonly for testing what happens, and nothing changed. npm run tsc exits without errors, too. It may be that this is no longer required due to other changes; it may also be that I just don't get it! Proposal: We ☎️ and inspect this.


/**
* The SVG path for the tip of the marker.
*/
tipPath: string

/**
* The viewBox for the marker.
*/
viewBox: string
}

export interface MarkerLayer {
Expand Down Expand Up @@ -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
}
59 changes: 32 additions & 27 deletions src/core/utils/map/setupMarkers.ts
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'
Expand All @@ -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'
Expand Down Expand Up @@ -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
)
)

Expand Down Expand Up @@ -162,6 +168,10 @@ export function setupMarkers(map: Map) {
return
}

getMarkerStyle = createGetMarkerStyle(() => {
map.render()
})

layers = configuration.layers.map((layer) =>
toMerged(
{
Expand Down Expand Up @@ -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
)
)
})
Expand All @@ -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)
Comment thread
warm-coolguy marked this conversation as resolved.
}
}
)
Expand All @@ -242,6 +252,7 @@ export function teardownMarkers(map: Map) {
stopWatcher = null
layers = []
lastClickEvent = null
getMarkerStyle = createGetMarkerStyle(() => {})

map.un('moveend', mapMoveEnd)
map.un('pointermove', mapPointerMove)
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please elaborate

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to propose this reordering:

	if (!feature) {
		store.hovered = null
		return
	}

	if (feature === toRaw(store.hovered)) {
		return
	}

	if (feature === toRaw(store.selected) || feature instanceof RenderFeature) {
		return
	}

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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)
}

Expand Down
Loading
Loading