Files
openfsd/internal/web/templates/airport_editor.html
Reese Norris dd04b4c004 web: airport-editor map layers and surface/aircraft lists (read-only)
Load APT/AIR, render OSM map layers, select for inspector view, fit bounds.
Hard scope: no draw tools or Blob download yet.
2026-07-23 19:52:28 -04:00

204 lines
10 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). Essential data path without JS: paste apt_text / air_text + form
CSRF echo-download. 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).
*/}}
<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">
<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-hint" data-js="chip-counts"></span>
</div>
<p class="apted-hint apted-title-hint">
Integrated .apt + .air · map requires JavaScript · text download works without JS
</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-secondary" data-js="fit">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>
</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">
Map editing requires JavaScript. Use the text fallback below to
paste and download <code>.apt</code> / <code>.air</code> files.
</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">Parse errors</span>
</div>
<div class="apted-panel-bd" data-js="panel-validate">
<p class="apted-hint">Load .apt / .air to see parse issues.</p>
</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">Read-only preview</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">View only</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>
{{/* No-JS fallback: always present in HTML (not JS-injected) */}}
<section class="apted-fallback" aria-labelledby="apted-fallback-heading">
<h2 id="apted-fallback-heading">Text fallback (works without JavaScript)</h2>
<p class="apted-hint">
Map editing requires JavaScript. Paste or type file text below, then download.
Echo-download only — the server does not persist these files.
</p>
<form method="post" action="/airport-editor/download-apt" class="apted-form">
<input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
<div class="apted-field">
<label for="apt_text">Airport .apt</label>
<textarea id="apt_text" name="apt_text" rows="12" spellcheck="false"
placeholder="icao=KBTV&#10;…"></textarea>
</div>
<div class="apted-field">
<label for="apt_filename">Download filename</label>
<input type="text" id="apt_filename" name="filename" value="airport.apt"
autocomplete="off" spellcheck="false" maxlength="64"
pattern="[A-Za-z0-9._\-]{1,64}">
</div>
<button type="submit" class="btn btn-sm btn-primary">Download .apt</button>
</form>
<form method="post" action="/airport-editor/download-air" class="apted-form">
<input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
<div class="apted-field">
<label for="air_text">Scenario .air</label>
<textarea id="air_text" name="air_text" rows="8" spellcheck="false"
placeholder="CALLSIGN:TYPE:…"></textarea>
</div>
<div class="apted-field">
<label for="air_filename">Download filename</label>
<input type="text" id="air_filename" name="filename" value="scenario.air"
autocomplete="off" spellcheck="false" maxlength="64"
pattern="[A-Za-z0-9._\-]{1,64}">
</div>
<button type="submit" class="btn btn-sm btn-primary">Download .air</button>
</form>
</section>
</main>
<script src="/static/js/leaflet.js"></script>
<script src="/static/js/openfsd/leaflet.rotatedmarker.js"></script>
<script type="module" src="/static/js/openfsd/airport-editor/main.js"></script>
{{ end }}