Skip to content

Add custom styling for geoman#134

Merged
giswqs merged 6 commits intomainfrom
styles
Nov 20, 2025
Merged

Add custom styling for geoman#134
giswqs merged 6 commits intomainfrom
styles

Conversation

@giswqs
Copy link
Copy Markdown
Member

@giswqs giswqs commented Nov 20, 2025

paint = {
    "line": {
        "line-color": [
            "match", ["coalesce", ["get", "highway"], ""],
            "tertiary",      "#f89c3c",
            "residential",   "#9e9e9e",
            "service",       "#8d8d8d",
            "footway",       "#66bb6a",
            "cycleway",      "#42a5f5",
            "proposed",      "#ab47bc",
            "construction",  "#ffa000",
               "#607d8b"
        ],
        "line-width": [
            "match", ["coalesce", ["get", "highway"], ""],
            "tertiary",      3.5,
            "residential",   2.5,
            "service",       2.0,
            "footway",       1.6,
            "cycleway",      1.8,
            "proposed",      2.2,
            "construction",  2.2,
                2.0
        ]
        # Optional dashed look for proposed/construction:
        # "line-dasharray": [
        #   "case",
        #   ["in", ["coalesce", ["get","highway"], ""], ["literal", ["proposed","construction"]]],
        #   ["literal", [2,2]],
        #   ["literal", [1,0.0001]]
        # ]
    },
    "point": {
        "circle-color": [
            "match", ["coalesce", ["get", "highway"], ""],
            "turning_circle", "#ffee58",
            "crossing",       "#ff7043",
            "bus_stop",       "#1976d2",
            "stop",           "#d32f2f",
                "#546e7a"
        ],
        "circle-radius": [
            "match", ["coalesce", ["get", "highway"], ""],
            "turning_circle", 4,
            "crossing",       4,
            "bus_stop",       5,
            "stop",           5,
                 4
        ],
        "circle-stroke-color": "#263238",
        "circle-stroke-width": 1
    }
}

import anymap
m = anymap.MapLibreMap(center=[-117.592133766, 47.653004], zoom=15.3)
m.add_basemap("Esri.WorldImagery", visible=True, before_id=m.first_symbol_layer_id)
m.add_geoman_control(show_info_box=True,  info_box_mode="click", paint=paint, paint_above_geoman=True)
m

Copilot AI review requested due to automatic review settings November 20, 2025 04:15
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Nov 20, 2025

@github-actions github-actions Bot temporarily deployed to pull request November 20, 2025 04:16 Inactive
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This pull request adds custom styling capabilities for Geoman-managed features by introducing a mirrored read-only GeoJSON layer that visualizes geoman_data with user-defined paint properties.

  • Adds paint and paint_above_geoman parameters to add_geoman_control() method
  • Implements JavaScript infrastructure to create and maintain synchronized style layers
  • Enables data-driven styling expressions for geoman features (e.g., styling by feature properties)

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 9 comments.

File Description
anymap/maplibre.py Adds paint and paint_above_geoman parameters to the add_geoman_control method with documentation
anymap/static/maplibre_widget.js Implements mirrored style layer system that syncs with geoman_data, including layer creation, data updates, and placement logic

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

const layers = (map.getStyle() && map.getStyle().layers) || [];
for (const ly of layers) {
const id = (ly && ly.id) || '';
if (id && (id.startsWith('gm-') || id.includes('geoman') || id.includes('gm'))) {
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

The layer detection logic for finding Geoman layers uses a broad heuristic (id.includes('geoman') || id.includes('gm')). This could potentially match non-Geoman layers that happen to contain 'gm' or 'geoman' in their IDs.

Consider making the detection more specific by:

  1. Checking for an exact prefix match only (e.g., id.startsWith('gm-') or id.startsWith('geoman-'))
  2. Maintaining a registry of actual Geoman layer IDs when they're created
  3. Checking for a specific attribute that marks Geoman layers

Example improvement:

// More specific prefix check
if (id && (id.startsWith('gm-') || id.startsWith('geoman-'))) {
  beforeId = id;
  break;
}
Suggested change
if (id && (id.startsWith('gm-') || id.includes('geoman') || id.includes('gm'))) {
if (id && (id.startsWith('gm-') || id.startsWith('geoman-'))) {

Copilot uses AI. Check for mistakes.
Comment thread anymap/static/maplibre_widget.js Outdated
Comment on lines +3954 to +3958
try {
if (el._geomanStyle && map.getSource(el._geomanStyle.srcId)) {
map.getSource(el._geomanStyle.srcId).setData(collection || { type: 'FeatureCollection', features: [] });
}
} catch (_e) {}
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

This is a duplicate of the code at lines 3924-3929. The mirrored style source update logic should be refactored to use the refreshGeomanStyleLayers function instead of being duplicated. See comment on lines 3924-3929 for the refactoring suggestion.

Suggested change
try {
if (el._geomanStyle && map.getSource(el._geomanStyle.srcId)) {
map.getSource(el._geomanStyle.srcId).setData(collection || { type: 'FeatureCollection', features: [] });
}
} catch (_e) {}
refreshGeomanStyleLayers(map, el, collection);

Copilot uses AI. Check for mistakes.
Comment thread anymap/static/maplibre_widget.js Outdated
Comment thread anymap/maplibre.py Outdated
Comment thread anymap/static/maplibre_widget.js Outdated
Comment on lines +4322 to +4404
const ensureGeomanStyleLayers = (paintConfig) => {
if (!paintConfig) return;
const srcId = 'geoman-style-src';
const fillId = 'geoman-style-fill';
const lineId = 'geoman-style-line';
const pointId = 'geoman-style-point';
try {
if (!map.getSource(srcId)) {
map.addSource(srcId, { type: 'geojson', data: { type: 'FeatureCollection', features: [] } });
}
} catch (_e) {}
// Placement: default above Geoman; if paintAbove === false, push below first geoman layer
let beforeId = undefined;
if (!paintAbove) {
try {
const layers = (map.getStyle() && map.getStyle().layers) || [];
for (const ly of layers) {
const id = (ly && ly.id) || '';
if (id && (id.startsWith('gm-') || id.includes('geoman') || id.includes('gm'))) {
beforeId = id;
break;
}
}
} catch (_e2) {}
}
if (!map.getLayer(fillId)) {
try {
const layer = {
id: fillId,
type: 'fill',
source: srcId,
filter: ['any',
['==', ['geometry-type'], 'Polygon'],
['==', ['geometry-type'], 'MultiPolygon']
],
paint: Object.assign({
'fill-color': '#90caf9',
'fill-opacity': 0.2
}, (paintConfig.fill || {}))
};
if (beforeId) map.addLayer(layer, beforeId); else map.addLayer(layer);
} catch (_e3) {}
}
if (!map.getLayer(lineId)) {
try {
const layer = {
id: lineId,
type: 'line',
source: srcId,
filter: ['any',
['==', ['geometry-type'], 'LineString'],
['==', ['geometry-type'], 'MultiLineString']
],
paint: Object.assign({
'line-color': '#42a5f5',
'line-width': 2.5
}, (paintConfig.line || {}))
};
if (beforeId) map.addLayer(layer, beforeId); else map.addLayer(layer);
} catch (_e4) {}
}
if (!map.getLayer(pointId)) {
try {
const layer = {
id: pointId,
type: 'circle',
source: srcId,
filter: ['any',
['==', ['geometry-type'], 'Point'],
['==', ['geometry-type'], 'MultiPoint']
],
paint: Object.assign({
'circle-radius': 5,
'circle-color': '#1976d2',
'circle-stroke-color': '#0d47a1',
'circle-stroke-width': 1.5
}, (paintConfig.point || {}))
};
if (beforeId) map.addLayer(layer, beforeId); else map.addLayer(layer);
} catch (_e5) {}
}
el._geomanStyle = { srcId, fillId, lineId, pointId };
};
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

[nitpick] The ensureGeomanStyleLayers function could be called multiple times, but it only checks if layers exist before creating them. Consider adding a guard to prevent multiple initialization attempts or track the initialization state more explicitly.

For example:

const ensureGeomanStyleLayers = (paintConfig) => {
  if (!paintConfig || el._geomanStyleInitialized) return;
  // ... rest of initialization
  el._geomanStyleInitialized = true;
};

This would make the initialization behavior more explicit and prevent any potential race conditions if the function is called concurrently.

Copilot uses AI. Check for mistakes.
Comment thread anymap/static/maplibre_widget.js Outdated
Comment on lines +4333 to +4346
// Placement: default above Geoman; if paintAbove === false, push below first geoman layer
let beforeId = undefined;
if (!paintAbove) {
try {
const layers = (map.getStyle() && map.getStyle().layers) || [];
for (const ly of layers) {
const id = (ly && ly.id) || '';
if (id && (id.startsWith('gm-') || id.includes('geoman') || id.includes('gm'))) {
beforeId = id;
break;
}
}
} catch (_e2) {}
}
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

The comment states "default above Geoman; if paintAbove === false, push below first geoman layer", but the logic is inverted. When paintAbove is false, the code searches for a Geoman layer to use as beforeId, which would place the style layers below (rendered first). However, when paintAbove is true (default), beforeId remains undefined, which adds layers on top (rendered last).

This appears to be working as intended based on the parameter documentation, but the comment is misleading. Consider updating the comment to:

// Placement: if paintAbove === false, insert below first geoman layer; otherwise add on top (default)

This would better reflect the actual behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +4856 to +4863
// Ensure mirrored style reflects current data
try {
if (stylePaint) {
ensureGeomanStyleLayers(stylePaint);
const currentData = model.get('geoman_data') || { type: 'FeatureCollection', features: [] };
refreshGeomanStyleLayers(currentData);
}
} catch (_e) {}
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

The initialization code calls ensureGeomanStyleLayers(stylePaint) twice in quick succession (line 4859 and originally at line 4414), which is redundant. The first call at line 4414 already ensures the layers exist and are initialized. The second call will exit early due to the !map.getLayer(fillId) checks, but this still represents unnecessary work.

Consider either:

  1. Removing the second ensureGeomanStyleLayers call and only calling refreshGeomanStyleLayers
  2. Adding a flag to track if initialization has already happened

Suggested fix:

// Ensure mirrored style reflects current data
try {
  if (stylePaint && el._geomanStyle) {
    // Layers already created at line 4414, just refresh data
    const currentData = model.get('geoman_data') || { type: 'FeatureCollection', features: [] };
    refreshGeomanStyleLayers(currentData);
  }
} catch (_e) {}

Copilot uses AI. Check for mistakes.
giswqs and others added 4 commits November 19, 2025 23:24
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@github-actions github-actions Bot temporarily deployed to pull request November 20, 2025 04:27 Inactive
@github-actions github-actions Bot temporarily deployed to pull request November 20, 2025 04:29 Inactive
@giswqs giswqs merged commit 7cfa4a1 into main Nov 20, 2025
8 checks passed
@giswqs giswqs deleted the styles branch November 20, 2025 04:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants