diff --git a/internal/web/static/js/openfsd/airport-editor/map-layers.js b/internal/web/static/js/openfsd/airport-editor/map-layers.js
index af114a1..ca0578c 100644
--- a/internal/web/static/js/openfsd/airport-editor/map-layers.js
+++ b/internal/web/static/js/openfsd/airport-editor/map-layers.js
@@ -236,31 +236,48 @@ export const VERTEX_PANE = 'aptedVertex';
/**
* Pure: Leaflet circleMarker options for a vertex node (always lat/lng-centered).
+ * Colors follow surfaceStyle kind palette so taxi / rwy / hold / park stay distinct.
* Drag is NOT Marker.draggable — OverlayController uses capture-phase map hit-test.
+ *
* @param {number} vertexIndex
- * @param {{ selected?: boolean, dragging?: boolean }} [state]
+ * @param {{ kind?: string, selected?: boolean, dragging?: boolean }} [state]
* @returns {object}
*/
export function buildVertexHandleStyle(vertexIndex, state = {}) {
const selected = !!state.selected;
const dragging = !!state.dragging;
+ const kind = state.kind || SurfaceTaxiway;
+ // Kind colors always (not selection-blue) so types stay readable when all nodes show.
+ const base = surfaceStyle(kind, false);
+ const stroke = base.color;
+ // Polylines: white disc + kind stroke. Parking: green fill like parking markers.
+ let fillColor = '#ffffff';
+ let fillOpacity = 1;
+ if (kind === SurfaceParking) {
+ fillColor = base.fillColor || '#40916c';
+ fillOpacity = 0.95;
+ }
+ if (dragging) {
+ // Slightly emphasize without losing kind hue.
+ fillColor = kind === SurfaceParking ? '#7dcea0' : '#ffffff';
+ }
return {
- radius: VERTEX_HANDLE_RADIUS,
+ radius: VERTEX_HANDLE_RADIUS + (selected ? 1 : 0) + (dragging ? 1 : 0),
// Interactive false: pointer events pass through to map capture hit-test.
interactive: false,
bubblingMouseEvents: false,
- weight: dragging ? 2 : 1.5,
+ weight: dragging ? 2.5 : selected ? 2 : 1.5,
opacity: 1,
- fillOpacity: 1,
- color: dragging ? '#1a3a60' : selected ? '#2a5a90' : '#4a7ab0',
- fillColor: dragging ? '#d0e4f8' : selected ? '#e8f0fa' : '#ffffff',
+ fillOpacity,
+ color: stroke,
+ fillColor,
className:
'apted-vertex-handle' +
+ ` apted-vertex-${kindShort(kind)}` +
(selected ? ' is-selected' : '') +
(dragging ? ' is-dragging' : ''),
- // title is not a path option; kept only for call-site docs
- // (vertexIndex used so the API stays stable for tests)
_vertexIndex: vertexIndex,
+ _kind: kind,
};
}
@@ -1114,9 +1131,12 @@ export class OverlayController {
mapDraggingWasEnabled,
};
- // Visual feedback on the centered circleMarker.
+ // Visual feedback on the centered circleMarker (keep kind color).
if (handle && typeof handle.setStyle === 'function') {
- handle.setStyle(buildVertexHandleStyle(vertexIndex, { selected: true, dragging: true }));
+ const kind = this._airport?.surfaces?.[surfaceIndex]?.kind;
+ handle.setStyle(
+ buildVertexHandleStyle(vertexIndex, { kind, selected: true, dragging: true }),
+ );
}
// Do not snap vertex to cursor on mousedown — only move once the pointer moves.
@@ -1152,7 +1172,14 @@ export class OverlayController {
if (h && typeof h.setStyle === 'function') {
const stillSelected =
this._selection?.type === 'surface' && this._selection.index === si;
- h.setStyle(buildVertexHandleStyle(vi, { selected: stillSelected, dragging: false }));
+ const kind = this._airport?.surfaces?.[si]?.kind;
+ h.setStyle(
+ buildVertexHandleStyle(vi, {
+ kind,
+ selected: stillSelected,
+ dragging: false,
+ }),
+ );
}
const ll = this._latLngFromPointerEvent(ev) || (h && h.getLatLng && h.getLatLng());
@@ -1194,6 +1221,7 @@ export class OverlayController {
const pts = surface.points || [];
const selected =
this._selection?.type === 'surface' && this._selection.index === surfaceIndex;
+ const kind = surface.kind;
for (let vi = 0; vi < pts.length; vi++) {
const p = pts[vi];
@@ -1201,7 +1229,7 @@ export class OverlayController {
// circleMarker: geographic center === visual center (no iconAnchor).
const handle = L.circleMarker(
[p.lat, p.lon],
- buildVertexHandleStyle(vi, { selected, dragging: false }),
+ buildVertexHandleStyle(vi, { kind, selected, dragging: false }),
);
handle.addTo(this.vertexGroup);
this._handleByKey.set(`${surfaceIndex}:${vi}`, handle);
diff --git a/internal/web/templates/airport_editor.html b/internal/web/templates/airport_editor.html
index 7646d34..3f8de65 100644
--- a/internal/web/templates/airport_editor.html
+++ b/internal/web/templates/airport_editor.html
@@ -213,7 +213,7 @@
Static assets are go:embed into the openfsd binary — restart the process after
JS/CSS changes. Query string busts browser cache when the binary is rebuilt.
*/}}
-
-
-
+
+
+
{{ end }}
diff --git a/webjs/airport-editor/map-layers.test.js b/webjs/airport-editor/map-layers.test.js
index 3c90d7a..bcfe596 100644
--- a/webjs/airport-editor/map-layers.test.js
+++ b/webjs/airport-editor/map-layers.test.js
@@ -361,21 +361,52 @@ test('stopLeafletClickBubble sets originalEvent._stopped (Leaflet map bubble gua
stopLeafletClickBubble({}, null);
});
-test('buildVertexHandleStyle: circleMarker centered on lat/lng (no iconAnchor)', () => {
+test('buildVertexHandleStyle: kind colors match surface palette', () => {
assert.equal(VERTEX_HANDLE_RADIUS, 5);
assert.equal(VERTEX_HANDLE_PX, VERTEX_HANDLE_RADIUS * 2);
- const style = buildVertexHandleStyle(2, { selected: false, dragging: false });
- assert.equal(style.radius, VERTEX_HANDLE_RADIUS);
- assert.equal(style.interactive, false);
- assert.equal(style.bubblingMouseEvents, false);
- assert.equal(style.fillOpacity, 1);
- assert.match(style.className, /apted-vertex-handle/);
- assert.ok(!style.className.includes('is-selected'));
- const sel = buildVertexHandleStyle(0, { selected: true, dragging: false });
+
+ const taxi = buildVertexHandleStyle(0, { kind: SurfaceTaxiway });
+ const rwy = buildVertexHandleStyle(0, { kind: SurfaceRunway });
+ const hold = buildVertexHandleStyle(0, { kind: SurfaceHold });
+ const park = buildVertexHandleStyle(0, { kind: SurfaceParking });
+
+ // Differentiated strokes (same palette as surfaceStyle unselected).
+ assert.equal(taxi.color, surfaceStyle(SurfaceTaxiway, false).color);
+ assert.equal(rwy.color, surfaceStyle(SurfaceRunway, false).color);
+ assert.equal(hold.color, surfaceStyle(SurfaceHold, false).color);
+ assert.equal(park.color, surfaceStyle(SurfaceParking, false).color);
+ assert.notEqual(taxi.color, rwy.color);
+ assert.notEqual(taxi.color, hold.color);
+ assert.notEqual(hold.color, park.color);
+
+ // Taxi/rwy/hold: white fill disc; parking: green fill like parking markers.
+ assert.equal(taxi.fillColor, '#ffffff');
+ assert.equal(rwy.fillColor, '#ffffff');
+ assert.equal(hold.fillColor, '#ffffff');
+ assert.equal(park.fillColor, surfaceStyle(SurfaceParking, false).fillColor);
+
+ assert.equal(taxi.interactive, false);
+ assert.match(taxi.className, /apted-vertex-taxi/);
+ assert.match(rwy.className, /apted-vertex-rwy/);
+ assert.match(hold.className, /apted-vertex-hold/);
+ assert.match(park.className, /apted-vertex-park/);
+
+ const sel = buildVertexHandleStyle(0, {
+ kind: SurfaceTaxiway,
+ selected: true,
+ });
assert.match(sel.className, /is-selected/);
- const drag = buildVertexHandleStyle(0, { selected: true, dragging: true });
+ assert.equal(sel.color, taxi.color); // selection keeps kind hue
+ assert.ok(sel.radius > taxi.radius);
+
+ const drag = buildVertexHandleStyle(0, {
+ kind: SurfaceHold,
+ selected: true,
+ dragging: true,
+ });
assert.match(drag.className, /is-dragging/);
- // Legacy divIcon helpers still export centered anchors if ever reused.
+ assert.equal(drag.color, hold.color);
+
const icon = buildVertexHandleIconOptions();
assert.equal(icon.iconAnchor[0] * 2, icon.iconSize[0]);
assert.equal(buildVertexHandleOptions(2).interactive, false);