Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
95bf915
feat: added markers styling
raven-wing Aug 17, 2026
c50fa7d
fixes
raven-wing Aug 19, 2026
242cb5f
fix popup
raven-wing Aug 19, 2026
c76d8ef
some icon fixes
raven-wing Aug 20, 2026
b99e8e5
fix lint
raven-wing Aug 20, 2026
e15aa03
fix linting
raven-wing Aug 20, 2026
30088a2
some fixes
raven-wing Aug 20, 2026
6b2bc2c
fixes
raven-wing Aug 20, 2026
049554d
some trims
raven-wing Aug 20, 2026
d063439
some fixes
raven-wing Aug 20, 2026
3e19819
added missing marker
raven-wing Aug 20, 2026
e289645
cleanup
raven-wing Aug 20, 2026
4ff6c8a
cleanup
raven-wing Aug 20, 2026
0c3ee6a
cleanup comment
raven-wing Aug 20, 2026
3597a66
more human readable icons
raven-wing Aug 20, 2026
3457d33
some cleanup
raven-wing Aug 20, 2026
d2096e4
cleanup
raven-wing Aug 20, 2026
121cd64
cleanup comments
raven-wing Aug 20, 2026
56d1b13
cleanup
raven-wing Aug 20, 2026
d62f19d
fixes
raven-wing Aug 20, 2026
e2a608a
fixes
raven-wing Aug 20, 2026
34a1f84
fixes
raven-wing Aug 20, 2026
41cfcac
fix
raven-wing Aug 20, 2026
309a79d
added missing files
raven-wing Aug 20, 2026
0063c97
fix
raven-wing Aug 20, 2026
fe0fb27
refactor
raven-wing Aug 20, 2026
50fad55
refactor
raven-wing Aug 20, 2026
3c0b60d
lot of files changes
raven-wing Aug 20, 2026
b676b67
fixes
raven-wing Aug 21, 2026
ff4dcd8
a lot of refactor
raven-wing Aug 21, 2026
bbbbd22
removed compress
raven-wing Aug 21, 2026
bda07f7
fix sonar errors
raven-wing Aug 21, 2026
746c996
fixes after review
raven-wing Aug 21, 2026
d316152
lint fixes
raven-wing Aug 21, 2026
77bf36e
fix for ci problems
raven-wing Aug 21, 2026
d45271c
fixes
raven-wing Aug 21, 2026
f51abbe
update
raven-wing Aug 21, 2026
c4c8821
missing file
raven-wing Aug 21, 2026
c31459e
less comments
raven-wing Aug 21, 2026
08a870b
less comments
raven-wing Aug 21, 2026
954c924
less comments
raven-wing Aug 21, 2026
fe9925f
little simplification
raven-wing Aug 21, 2026
db52516
simplify
raven-wing Aug 21, 2026
092ace9
less code
raven-wing Aug 21, 2026
b606ebf
fif
raven-wing Aug 21, 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: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ Afterwards run it with:

| Option | Description |
|--------------------------|------------------------------------------------------------------------------------------------------------------------------------|
| USE_LAZY_LOADING | Loads point data only after the user clicks a point. If set to false, point data is loaded together with the initial map. |
| FAKE_LOGIN | If set to true, allows access to the admin panel by simply selecting the role instead of logging in. **DO NOT USE IN PRODUCTION!** |
| SHOW_ACCESSIBILITY_TABLE | If set as true it shows special view to help with accessing application. |

Expand Down
18 changes: 1 addition & 17 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ Everything below in one file — copy it and delete what you do not need:
max_size: 5242880 # 5 MiB

FEATURE_FLAGS:
USE_LAZY_LOADING: true
CATEGORIES_HELP: true
SHOW_SEARCH_BAR: true
SHOW_SUGGEST_NEW_POINT_BUTTON: true
Expand Down Expand Up @@ -165,8 +164,7 @@ Basic keys
Feature flags
-------------

``FEATURE_FLAGS`` is a flat mapping of flag name to boolean. Unset flags are off, with one
exception: ``USE_LAZY_LOADING`` defaults to on.
``FEATURE_FLAGS`` is a flat mapping of flag name to boolean. Unset flags are off.

Flags fall into two groups: some change what the backend does, others are handed to the
frontend to decide what to render. Both are set the same way.
Expand All @@ -178,13 +176,6 @@ frontend to decide what to render. Both are set the same way.
* - Flag
- Acts on
- Effect
* - ``USE_LAZY_LOADING``
- backend
- **On by default.** Builds the location model from ``location_obligatory_fields``
and ``categories`` in your data source, so submitted points are validated against
them, and the "suggest a new point" form is generated from them. Set it to
``false`` and only ``uuid``, ``position`` and ``remark`` are validated, and the
suggest form has no fields — see the note below.
* - ``CATEGORIES_HELP``
- both
- Enables the help-tooltip data in ``/api/categories-full``, and makes the frontend
Expand Down Expand Up @@ -219,13 +210,6 @@ frontend to decide what to render. Both are set the same way.
Never enable ``FAKE_LOGIN`` in production. It hands a logged-in session to anyone who
asks for one.

.. note::

``USE_LAZY_LOADING`` is named for behaviour that is now unconditional: point details
have their own endpoint (``/api/location/<uuid>``) whether the flag is set or not.
What the flag still controls is schema validation, as described above. Leave it on
unless you have a reason not to.

The frontend receives the whole ``FEATURE_FLAGS`` mapping, so a plugin or a custom build
can read flags Goodmap itself does not know about.

Expand Down
82 changes: 71 additions & 11 deletions docs/data-source.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ and their schema, alongside platzky's ``site_content`` section:

Note that ``plugins`` is a **sibling** of ``map``, not a key inside it.

Only ``data`` and ``categories`` are structurally required; ``suggestions`` and
``reports`` are created by the app as users submit things.
Only ``data`` is structurally required. ``categories`` defaults to no categories
if omitted (a map with only plain, unfiltered points is a valid setup); ``suggestions``
and ``reports`` are created by the app as users submit things.

Points
------
Expand Down Expand Up @@ -63,9 +64,9 @@ ordinary field of your own:
Used as the marker popup's **subtitle**.

``remark`` (optional)
Free text. Its presence — not its content — is exposed by ``/api/locations`` as a
boolean, so the frontend can flag points that have something noteworthy without
fetching them all.
Free text. Its presence — not its content — is exposed by ``/api/locations`` as
``marker.badge: true`` (see :ref:`data-source-marker-styles`), so the frontend can
flag points that have something noteworthy without fetching them all.

Everything else is yours. Custom fields are only *shown* if you list them in
``visible_data``, and only *filterable* if you list them in ``categories``.
Expand All @@ -79,6 +80,8 @@ Everything else is yours. Custom fields are only *shown* if you list them in
Field schema
------------

.. _data-model-location_obligatory_fields:

``location_obligatory_fields``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -108,12 +111,6 @@ This drives three things at once:
- **Length limits.** String fields are capped at 200 characters, lists at 20 items of at
most 100 characters each.

.. important::

This key is only read when the ``USE_LAZY_LOADING`` feature flag is on. With it off,
nothing beyond ``uuid``/``position``/``remark`` is validated and the suggest form comes
up empty. See :ref:`config-feature-flags`.

.. _data-model-visible_data:

``visible_data`` and ``meta_data``
Expand Down Expand Up @@ -260,6 +257,69 @@ Each category's active mode is exposed as ``filter_mode`` in the
``/api/categories-full`` response, so a custom frontend can render the right control —
checkbox or radio — without hardcoding category names.

.. _data-source-marker-styles:

Marker styles
-------------

``marker_styles`` picks which of your fields drive each point's pin icon and color, and
supplies the lookup tables those values are resolved through. It is entirely optional —
a map with no ``marker_styles`` still renders, just with plain pins.

.. code-block:: json

{
"marker_styles": {
"icon_field": "type_of_place",
"color_field": "transparency",
"icon_provider": "phosphor",
"icons": {
"big bridge": "bridge",
"container": "shipping-container"
},
"colors": {
"lacking": "#c62828",
"full": "#2e7d32"
}
}
}

``icon_field``, ``color_field``
Names of fields on your points whose *value* selects the icon/color for that point.
Either or both may be omitted.

Each must be declared in :ref:`data-model-location_obligatory_fields` — a field
every point is guaranteed to have, so that styling is never driven by something
only some of your points carry. A name that isn't declared there is ignored, and
pins get no icon/color from it.

``icon_provider``
Where your icons come from. One provider serves the whole ``icons`` table:

``phosphor``
Entries are `Phosphor <https://phosphoricons.com/>`_ icon names in kebab-case
(e.g. ``"shipping-container"``), so you need not host SVGs yourself.
``url``
Entries are URLs of SVGs you host.

Required whenever ``icons`` has anything in it. Naming a provider GoodMap does not
know stops the app from starting, so the mistake surfaces on deploy rather than as a
silently unstyled pin.

``icons``
Maps a value of ``icon_field`` to whatever ``icon_provider`` takes — an icon name for
``phosphor``, a URL for ``url``. GoodMap turns these into finished URLs when the app
starts, so the browser never sees the provider and adding a new one needs no frontend
release.

``colors``
Maps a value of ``color_field`` to a CSS color.

A point whose ``icon_field``/``color_field`` value has no entry in ``icons``/``colors``
simply renders without that part of the styling — this is not an error. A point with a
``remark`` (see above) always gets the asterisk badge regardless of whether its icon/color
matched anything.

User submissions
----------------

Expand Down
16 changes: 10 additions & 6 deletions docs/http-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,11 @@ Query parameters:

curl 'http://localhost:5000/api/locations?accessible_by=bikes&lat=51.10&lon=17.05&limit=5'

Each point comes back as ``uuid``, ``position`` and ``has_remark`` — a **boolean**, whether
the point has a remark, not its text.
Each point comes back as ``uuid`` and ``position``, plus a ``marker`` object with the pin
styling: ``icon``/``color`` (the raw values of whichever fields this deployment's
``marker_styles`` config names, see :ref:`data-source-marker-styles`) and ``badge: true``
when the point has a remark. ``marker`` is left out entirely when none of that applies to
a point, and inside it each key is left out rather than sent as ``null``/``false``.

A ``lat``, ``lon`` or ``limit`` that cannot mean anything — not a number, or outside the
range above — is a ``400 {"message": "Invalid request data"}`` rather than a silently
Expand All @@ -147,10 +150,11 @@ Takes every parameter of :ref:`api-locations`, plus ``zoom`` (integer, **0–16*
bad ``lat``, is a ``400``.

Points and clusters come back in one list, told apart by ``type``. A ``"point"`` carries
a real ``uuid`` you can pass to :ref:`api-location-detail`; a ``"cluster"`` carries a
freshly-generated ``cluster_uuid`` (not stable across requests — it is a render key, not
an identifier) and the number of points it stands for. ``position`` is
``[latitude, longitude]``, as everywhere else.
a real ``uuid`` you can pass to :ref:`api-location-detail`, plus the same ``marker`` object
as ``/api/locations``; a ``"cluster"`` carries a freshly-generated ``cluster_uuid`` (not
stable across requests — it is a render key, not an identifier) and the number of points
it stands for, but no ``marker``. ``position`` is ``[latitude, longitude]``, as everywhere
else.

.. _api-location-detail:

Expand Down
1 change: 0 additions & 1 deletion docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@ Create ``config.yml`` next to it:
PATH: data.json

FEATURE_FLAGS:
USE_LAZY_LOADING: true
SHOW_SEARCH_BAR: true
SHOW_SUGGEST_NEW_POINT_BUTTON: true

Expand Down
1 change: 0 additions & 1 deletion e2e-tests/e2e_stress_test_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ LANGUAGES:
country: PL

FEATURE_FLAGS:
USE_LAZY_LOADING: True
SHOW_ACCESSIBILITY_TABLE: True
USE_SERVER_SIDE_CLUSTERING: False
CATEGORIES_HELP: True
Expand Down
1 change: 0 additions & 1 deletion e2e-tests/e2e_test_config.template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ LANGUAGES:
country: PL

FEATURE_FLAGS:
USE_LAZY_LOADING: True
SHOW_ACCESSIBILITY_TABLE: True
USE_SERVER_SIDE_CLUSTERING: False
CATEGORIES_HELP: True
Expand Down
17 changes: 16 additions & 1 deletion e2e-tests/e2e_test_data_initial.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@
"is_free": "true",
"speed_limit": "10",
"amenities": [
"benches"
"benches",
"toilets"
],
"uuid": "5986e755-1eaa-4121-a01c-4fef1d5d1da1"
},
Expand Down Expand Up @@ -272,6 +273,20 @@
"cars"
]
},
"marker_styles": {
"icon_field": "type_of_place",
"color_field": "speed_limit",
"icon_provider": "phosphor",
"icons": {
"big bridge": "bridge",
"small bridge": "footprints"
},
"colors": {
"10": "#2e7d32",
"30": "#ef6c00",
"50": "#c62828"
}
},
"visible_data": [
"remark",
"accessible_by",
Expand Down
105 changes: 105 additions & 0 deletions e2e-tests/tests/basic/test_marker_styles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""
Marker Styles Tests

Tests that the map picks pin icon/color per marker_styles (icon_field:
type_of_place, color_field: speed_limit - see e2e_test_data_initial.json), and
that a location with both a remark and a marker_styles match keeps its
type/color styling with an asterisk badge overlay, rather than losing it to a
plain, unstyled asterisk badge (see getTypedMarkerIcon.jsx/MarkerPopup.jsx).
"""

from playwright.sync_api import Page, expect

from tests.conftest import BASE_URL, MARKER_LOAD_TIMEOUT, open_test_popup

# "big bridge" and "small bridge" each get their own Phosphor Icons (MIT) type
# icon - see e2e_test_data_initial.json's marker_styles.icons ({provider:
# "phosphor", value: "..."}, resolved to a jsdelivr CDN URL - see
# resolvePhosphorIconUrl.js) and getTypedMarkerIcon.jsx (icon URLs are CSS
# mask-image'd onto the pin, tinted by the matched color, rather than embedded
# as inline SVG path data).
PHOSPHOR_ICONS_CDN_BASE = "https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2/assets/fill"
BIG_BRIDGE_TYPE_ICON_URL = f"{PHOSPHOR_ICONS_CDN_BASE}/bridge-fill.svg"
SMALL_BRIDGE_TYPE_ICON_URL = f"{PHOSPHOR_ICONS_CDN_BASE}/footprints-fill.svg"


class TestMarkerStyles:
"""Test suite for marker_styles-driven pin icons/colors"""

def test_fast_bridge_marker_uses_type_icon_and_red_speed_color(self, page: Page):
"""Pokoju (big bridge, speed_limit=50, no remark) is the only seeded bridge
with all three of lighting+benches+toilets (amenities is an "and" category -
see test_and_filter_within_category_narrows_results in test_map.py), so
checking all three isolates its marker without relying on clustering
distance/zoom assumptions."""
page.goto(BASE_URL, wait_until="domcontentloaded")

# "cars" is checked by default (Pokoju is cars-accessible); narrow further.
for amenity in ("lighting", "benches", "toilets"):
page.get_by_role("checkbox", name=amenity, exact=False).click()

marker = page.locator(".custom-typed-marker-icon")
expect(marker).to_have_count(1, timeout=MARKER_LOAD_TIMEOUT)

# The pin shape itself (a masked div, not an inline <path>), filled with
# speed_limit=50's color.
pin = marker.locator(".custom-typed-marker-pin")
expect(pin).to_have_css("background-color", "rgb(198, 40, 40)") # #c62828
# The type_of_place icon, configured for "big bridge" - masked onto a div
# via CSS rather than embedded as an inline <path>.
type_icon = marker.locator(".custom-typed-marker-type-icon")
expect(type_icon).to_have_count(1)
expect(type_icon).to_have_css("mask-image", f'url("{BIG_BRIDGE_TYPE_ICON_URL}")')
# No remark on Pokoju, so no asterisk badge.
expect(marker.locator("span")).to_have_count(0)

def test_slow_bridge_marker_uses_type_icon_and_green_speed_color(self, page: Page):
"""Piaskowy (small bridge, speed_limit=10, no remark, toilets) is the
only seeded speed<=10 bridge with toilets - the other two speed=10
bridges (Zwierzyniecka, Tumski) have lighting/benches but neither has
toilets, so combining the speed_limit=10 radio with the toilets
checkbox isolates it without relying on clustering distance/zoom
assumptions. "cars" is unchecked first since Piaskowy is
pedestrians-only."""
page.goto(BASE_URL, wait_until="domcontentloaded")

page.get_by_role("checkbox", name="cars", exact=False).click()
page.get_by_role("radio", name="10 km/h", exact=False).click()
page.get_by_role("checkbox", name="toilets", exact=False).click()

marker = page.locator(".custom-typed-marker-icon")
expect(marker).to_have_count(1, timeout=MARKER_LOAD_TIMEOUT)

pin = marker.locator(".custom-typed-marker-pin")
expect(pin).to_have_css("background-color", "rgb(46, 125, 50)") # #2e7d32 (speed_limit=10)
type_icon = marker.locator(".custom-typed-marker-type-icon")
expect(type_icon).to_have_count(1)
expect(type_icon).to_have_css("mask-image", f'url("{SMALL_BRIDGE_TYPE_ICON_URL}")')
# No remark on Piaskowy, so no asterisk badge.
expect(marker.locator("span")).to_have_count(0)

def test_remarked_bridge_keeps_type_and_color_styling_with_asterisk_badge(self, page: Page):
"""Zwierzyniecka has both a remark and marker_styles-matching fields
(small bridge, speed_limit=10) - it should render its normal typed/colored
pin plus an asterisk badge, not fall back to our own pin in the plain
fallback color with no type icon (every type_of_place/speed_limit value
happens to be covered by marker_styles in this seeded dataset, so that
fallback-color path isn't exercised here - it's covered at the unit
level instead, see getTypedMarkerIcon.test.jsx's "returns our own pin in
the fallback color with just the badge" case). Also guards against ever
reintroducing the old PNG-based asterisk icon this replaced.
"""
page.goto(BASE_URL, wait_until="domcontentloaded")
open_test_popup(page)

expect(page.locator('img[alt="Marker-Asterisk"]')).to_have_count(0)

marker = page.locator(".custom-typed-marker-icon")
expect(marker).to_have_count(1, timeout=MARKER_LOAD_TIMEOUT)

pin = marker.locator(".custom-typed-marker-pin")
expect(pin).to_have_css("background-color", "rgb(46, 125, 50)") # #2e7d32 (speed_limit=10)
type_icon = marker.locator(".custom-typed-marker-type-icon")
expect(type_icon).to_have_count(1)
expect(type_icon).to_have_css("mask-image", f'url("{SMALL_BRIDGE_TYPE_ICON_URL}")')
expect(marker.locator("span")).to_have_text("*")
1 change: 0 additions & 1 deletion examples/e2e_test_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ LANGUAGES:
country: PL

FEATURE_FLAGS:
USE_LAZY_LOADING: True
USE_SERVER_SIDE_CLUSTERING: False
SHOW_ACCESSIBILITY_TABLE: True
FAKE_LOGIN: False
Expand Down
Loading
Loading