Files
openfsd/internal/web/templates/airport_editor.html
Reese Norris e8c6965fe0 airport-editor: fix selection clearing on feature click (Leaflet _stopped)
Root cause: clicking a taxiway selected it (blue highlight) then the same
DOM click re-fired on the map and cleared selection, so handles never stayed
visible and vertex drag could not run.

Leaflet only stops map re-fire when originalEvent._stopped is set; native
stopPropagation alone does not set that flag. Set _stopped, disable path
bubblingMouseEvents, and gate map clicks after feature clicks. Disable boxZoom
(blue rectangle). Cache-bust static module URLs (assets are go:embed).
2026-07-27 15:22:44 -04:00

219 lines
12 KiB
HTML

{{ define "title" }}Airport Editor{{ end }}
{{ define "body" }}
{{/*
JS budget exception (airport map editor) — complexity-gate exception:
This route loads Leaflet (vendor) + first-party airport-editor modules.
Map geometry authoring is JS-primary (documented exception to the general
PE bar): open/edit/download requires JavaScript. No SPA router / global
server store / hydration. Files are never written to server disk or DB.
e2e blank tiles: set data-test-tiles="blank" on #apted-root (no OSM/Esri).
That is the only blank-tile hook for this page (query ?e2e=1 is not used).
*/}}
<link href="/static/css/leaflet.css" rel="stylesheet">
<link href="/static/css/openfsd/airport-editor.css" rel="stylesheet">
<main id="apted-root" class="apted container-fluid"
data-js="airport-editor"
data-validate-apt="/api/v1/editor/validate-apt"
data-validate-air="/api/v1/editor/validate-air">
<header class="apted-titlebar">
<h1>Airport Editor</h1>
<div class="apted-chips" aria-label="Document status">
<span class="apted-chip apted-chip-icao" data-js="chip-icao"></span>
<span class="apted-chip" data-js="chip-apt" title="Airport geometry">APT</span>
<span class="apted-chip" data-js="chip-air" title="Scenario aircraft">AIR</span>
<span class="apted-chip apted-chip-issues is-empty" data-js="chip-issues"
title="Load APT/AIR to validate"></span>
<span class="apted-hint" data-js="chip-counts"></span>
</div>
<p class="apted-hint apted-title-hint">
Integrated .apt + .air · requires JavaScript · downloads via toolbar (Blob)
</p>
</header>
{{ if .FlashSuccess }}
<div class="apted-flash apted-flash-ok" role="status">{{ .FlashSuccess }}</div>
{{ end }}
{{ if .FlashError }}
<div class="apted-flash apted-flash-err" role="alert">{{ .FlashError }}</div>
{{ end }}
<p class="apted-flash" data-js="status" hidden role="status"></p>
<div class="apted-toolbar" role="toolbar" aria-label="Editor tools">
<label class="btn btn-sm btn-outline-dark apted-file-btn">
Open .apt
<input type="file" accept=".apt,text/plain" data-js="open-apt" hidden>
</label>
<label class="btn btn-sm btn-outline-dark apted-file-btn">
Open .air
<input type="file" accept=".air,text/plain" data-js="open-air" hidden>
</label>
<span class="apted-toolbar-sep" aria-hidden="true"></span>
<button type="button" class="btn btn-sm btn-outline-primary" data-js="dl-apt"
title="Download .apt (Blob)">DL .apt</button>
<button type="button" class="btn btn-sm btn-outline-primary" data-js="dl-air"
title="Download .air (Blob)">DL .air</button>
<button type="button" class="btn btn-sm btn-outline-secondary" data-js="new"
title="New empty document">New</button>
<button type="button" class="btn btn-sm btn-outline-secondary" data-js="mark-clean"
title="Mark APT/AIR clean without download">Mark clean</button>
<span class="apted-toolbar-sep" aria-hidden="true"></span>
<button type="button" class="btn btn-sm btn-outline-secondary" data-js="fit"
title="Fit bounds (F)">Fit</button>
<button type="button" class="btn btn-sm btn-outline-secondary" data-js="layer"
aria-label="Basemap layer: OSM. Click to switch.">Layer: OSM</button>
<span class="apted-toolbar-sep" aria-hidden="true"></span>
<div class="apted-mode-group" role="group" aria-label="Edit mode" data-js="mode-group">
<button type="button" class="btn btn-sm btn-outline-dark apted-mode-btn is-active"
data-mode="select" aria-pressed="true" title="Select (1)">1 Select</button>
<button type="button" class="btn btn-sm btn-outline-dark apted-mode-btn"
data-mode="park" aria-pressed="false" title="Parking (2)">2 Park</button>
<button type="button" class="btn btn-sm btn-outline-dark apted-mode-btn"
data-mode="taxi" aria-pressed="false" title="Taxiway (3)">3 Taxi</button>
<button type="button" class="btn btn-sm btn-outline-dark apted-mode-btn"
data-mode="runway" aria-pressed="false" title="Runway (4)">4 Rwy</button>
<button type="button" class="btn btn-sm btn-outline-dark apted-mode-btn"
data-mode="hold" aria-pressed="false" title="Hold (5)">5 Hold</button>
<button type="button" class="btn btn-sm btn-outline-dark apted-mode-btn"
data-mode="aircraft" aria-pressed="false" title="Aircraft (6)">6 Acft</button>
</div>
</div>
<div class="apted-body">
<div id="apted-map" class="apted-map" role="application"
aria-label="Airport map editor (requires JavaScript)">
<p class="apted-map-placeholder">
Airport editor requires JavaScript to open, edit, and download
<code>.apt</code> / <code>.air</code> files on the map.
</p>
</div>
<aside class="apted-rail" data-js="rail" aria-label="Editor panels">
<div class="apted-tabs" role="tablist" aria-label="Editor tabs" data-js="rail-tabs">
<button type="button" class="apted-tab is-active" role="tab"
data-tab="airport" aria-selected="true" id="apted-tab-airport">Airport</button>
<button type="button" class="apted-tab" role="tab"
data-tab="surfaces" aria-selected="false" id="apted-tab-surfaces">Surfaces</button>
<button type="button" class="apted-tab" role="tab"
data-tab="aircraft" aria-selected="false" id="apted-tab-aircraft">Aircraft</button>
<button type="button" class="apted-tab" role="tab"
data-tab="validate" aria-selected="false" id="apted-tab-validate">Validate</button>
<button type="button" class="apted-tab" role="tab"
data-tab="raw" aria-selected="false" id="apted-tab-raw">Raw</button>
</div>
<div class="apted-rail-scroll">
<section class="apted-panel apted-tab-panel is-active" data-js-tab-panel="airport"
role="tabpanel" aria-labelledby="apted-tab-airport">
<div class="apted-panel-hd">
<h2>Airport</h2>
<span class="apted-hint">Headers</span>
</div>
<div class="apted-panel-bd" data-js="panel-airport">
<p class="apted-hint">Open a .apt file to view airport headers.</p>
</div>
</section>
<section class="apted-panel apted-tab-panel" data-js-tab-panel="surfaces" hidden
role="tabpanel" aria-labelledby="apted-tab-surfaces">
<div class="apted-panel-hd">
<h2>Surfaces</h2>
<span class="apted-hint">Click to select</span>
</div>
<div class="apted-panel-bd" data-js="panel-surfaces">
<p class="apted-hint">No surfaces.</p>
</div>
</section>
<section class="apted-panel apted-tab-panel" data-js-tab-panel="aircraft" hidden
role="tabpanel" aria-labelledby="apted-tab-aircraft">
<div class="apted-panel-hd">
<h2>Aircraft</h2>
<span class="apted-hint">Click to select</span>
</div>
<div class="apted-panel-bd" data-js="panel-aircraft">
<p class="apted-hint">No aircraft.</p>
</div>
</section>
<section class="apted-panel apted-tab-panel" data-js-tab-panel="validate" hidden
role="tabpanel" aria-labelledby="apted-tab-validate">
<div class="apted-panel-hd">
<h2>Validate</h2>
<span class="apted-hint">Client + server</span>
</div>
<div class="apted-panel-bd" data-js="panel-validate">
<p class="apted-hint apted-validate-summary" data-js="validate-summary">
No APT/AIR loaded. Open files or draw on the map to see live parse
errors and soft cross-file warnings.
</p>
<div class="apted-server-validate" data-js="server-validate">
<h3 class="apted-subhd">Server confirm</h3>
<p class="apted-hint">
Optional Go ParseAPT / ParseAIR check via Admin API (does not save).
</p>
<div class="apted-btn-row">
<button type="button" class="btn btn-sm btn-outline-primary"
data-action="confirm-server" data-js="confirm-server"
disabled
title="Requires JavaScript and a loaded document">
Confirm with server
</button>
</div>
</div>
<div class="apted-handoff" data-js="sweatbox-handoff">
<p class="apted-hint">
When ready: Download files, then load in
<a href="/sweatbox" data-js="sweatbox-link">Sweatbox</a>.
</p>
</div>
</div>
</section>
<section class="apted-panel apted-tab-panel" data-js-tab-panel="raw" hidden
role="tabpanel" aria-labelledby="apted-tab-raw">
<div class="apted-panel-hd">
<h2>Raw</h2>
<span class="apted-hint">Preview · Apply to re-parse</span>
</div>
<div class="apted-panel-bd apted-raw-panel">
<div class="apted-field">
<label for="apted-raw-apt">.apt (formatted)</label>
<textarea id="apted-raw-apt" data-js="raw-apt" rows="10"
readonly spellcheck="false"></textarea>
</div>
<div class="apted-field">
<label for="apted-raw-air">.air (formatted)</label>
<textarea id="apted-raw-air" data-js="raw-air" rows="6"
readonly spellcheck="false"></textarea>
</div>
</div>
</section>
<section class="apted-panel apted-inspector-panel" aria-labelledby="apted-inspector-heading">
<div class="apted-panel-hd">
<h2 id="apted-inspector-heading">Inspector</h2>
<span class="apted-hint">Edit fields · Del deletes</span>
</div>
<div class="apted-panel-bd" data-js="inspector">
<p class="apted-hint">Select a surface or aircraft on the map or in a list.</p>
</div>
</section>
</div>
</aside>
</div>
</main>
{{/*
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.
*/}}
<script src="/static/js/leaflet.js?v=vertex-edit-3"></script>
<script src="/static/js/openfsd/leaflet.rotatedmarker.js?v=vertex-edit-3"></script>
<script type="module" src="/static/js/openfsd/airport-editor/main.js?v=vertex-edit-3"></script>
{{ end }}