Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 3 additions & 8 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 @@ -108,12 +109,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
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
15 changes: 14 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,7 @@
"is_free": "true",
"speed_limit": "10",
"amenities": [
"benches"
"toilets"
],
"uuid": "5986e755-1eaa-4121-a01c-4fef1d5d1da1"
},
Expand Down Expand Up @@ -272,6 +272,19 @@
"cars"
]
},
"marker_styles": {
"icon_field": "type_of_place",
"color_field": "speed_limit",
"icons": {
"big bridge": "https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2/assets/fill/bridge-fill.svg",
"small bridge": "https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2/assets/fill/footprints-fill.svg"
},
"colors": {
"10": "#2e7d32",
"30": "#ef6c00",
"50": "#c62828"
}
},
"visible_data": [
"remark",
"accessible_by",
Expand Down
106 changes: 106 additions & 0 deletions e2e-tests/tests/basic/test_marker_styles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""
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 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).
BIG_BRIDGE_TYPE_ICON_URL = (
"https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2/assets/fill/bridge-fill.svg"
)
SMALL_BRIDGE_TYPE_ICON_URL = (
"https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2/assets/fill/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
14 changes: 14 additions & 0 deletions frontend/src/components/Map/store/markerStyles.store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { create } from 'zustand';

/**
* uuid -> resolved marker-styling field values (whatever marker_styles.icon_field/
* color_field point at), lazily fetched once a client-side-clustered marker becomes
* individually visible - see lazy-load-marker-styling-plan.md. A uuid with no
* matching styling is still recorded, as {}, so it isn't re-requested forever.
*/
const useMarkerStylesStore = create(set => ({
stylesByUuid: {},
mergeStyles: styles => set(state => ({ stylesByUuid: { ...state.stylesByUuid, ...styles } })),
}));

export default useMarkerStylesStore;
33 changes: 14 additions & 19 deletions frontend/src/components/MarkerPopup/MarkerPopup.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
import PropTypes from 'prop-types';
import { Marker } from 'react-leaflet';
import { isMobile } from 'react-device-detect';
import { Icon } from 'leaflet';
import { useTranslation } from 'react-i18next';
import httpService from '../../services/http/httpService';
import useMapStore from '../Map/store/map.store';
import useMarkerStylesStore from '../Map/store/markerStyles.store';

import LocationDetailsBox from './LocationDetails';
import MobilePopup from './MobilePopup';
import DesktopPopup from './DesktopPopup';
import iconAsterisk from '../../res/img/marker-icon-asterisk.png';
import getTypedMarkerIcon from './getTypedMarkerIcon';
import requestMarkerStyle from './requestMarkerStyle';

/**
* Wrapper component that fetches full location details and renders them in a popup.
Expand All @@ -37,7 +38,7 @@
}
} catch (error) {
if (isMounted) {
console.error('Failed to fetch location:', error);

Check warning on line 41 in frontend/src/components/MarkerPopup/MarkerPopup.jsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
setPlace({ error: true });
}
}
Expand Down Expand Up @@ -69,30 +70,20 @@
}).isRequired,
};

/**
* Custom Leaflet icon for markers with remarks/special annotations.
* Displays an asterisk icon to visually distinguish remarked locations from standard markers.
*/
const asteriskIcon = new Icon({
iconUrl: iconAsterisk,
iconSize: [40, 48], // size of the icon
iconAnchor: [19, 46], // point of the icon which will correspond to marker's location
popupAnchor: [0, -40], // point from which the popup should open relative to the iconAnchor
});

/**
* Interactive map marker component that displays location details in a popup when clicked.
* Supports special visual indication for locations with remarks using an asterisk icon.
*
* @param {Object} props - Component props
* @param {Object} props.place - Location data object
* @param {number[]} props.place.position - Coordinates [latitude, longitude]
* @param {boolean} [props.place.has_remark] - Whether this location has a remark (uses asterisk icon if true)
* @param {boolean} [props.place.has_remark] - Whether this location has a remark (adds an asterisk badge if true)
* @returns {React.ReactElement} Leaflet Marker component with click-to-show-details functionality
*/
const MarkerPopup = ({ place }) => {
const selectedLocationId = useMapStore(state => state.selectedLocationId);
const setSelectedLocationId = useMapStore(state => state.setSelectedLocationId);
const lazyMarkerStyle = useMarkerStylesStore(state => state.stylesByUuid[place.uuid]);
const [isClicked, setIsClicked] = useState(false);

// TODO: this only opens the popup if `place`'s Marker is actually attached to
Expand All @@ -114,18 +105,22 @@
setIsClicked(true);
};

const handleMarkerVisible = () => {
requestMarkerStyle(place.uuid);
};

const markerProps = {
position: place.position,
eventHandlers: {
click: handleMarkerClick,
add: handleMarkerVisible,
},
alt: place.has_remark ? 'Marker-Asterisk' : 'Marker',
};

// Only add icon prop if we have a custom icon (for remarks)
// This prevents passing undefined which can cause issues with MarkerClusterGroup
if (place.has_remark) {
markerProps.icon = asteriskIcon;
const styledPlace = lazyMarkerStyle ? { ...place, ...lazyMarkerStyle } : place;
const typedIcon = getTypedMarkerIcon(styledPlace);
if (typedIcon) {
markerProps.icon = typedIcon;
}

return (
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/MarkerPopup/ReportProblemForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ const ReportProblemForm = ({ placeId }) => {
if (schemaError) {
return (
<ErrorMessage>
{t('loadReportFormError')}
<span role="alert">{t('loadReportFormError')}</span>
<div>
<RetryButton type="button" onClick={refetchLocationSchema}>
{t('retry')}
Expand Down
Loading
Loading