Directions: add Imperial unit system (feet/miles) alongside metric
All checks were successful
CI / test-and-build (push) Successful in 39s

- Add a System selector (Metric/Imperial) to the Directions panel; Units
  dropdown adapts (m/km vs ft/mi) with a smart Auto for each system
- Auto: short routes show the small unit (m or ft), long routes the large
  one (km or mi); shows the alternate unit as a sub-line
- README: document the unit system + units options
This commit is contained in:
hermes-explorigin 2026-08-09 16:01:47 +00:00
parent 4b00704735
commit 8c2e7eb6ab
2 changed files with 67 additions and 17 deletions

View File

@ -230,8 +230,9 @@ button appears in the top bar. Opening it lets you:
- **Avoid certain POI categories** along the route (schools, fire stations, - **Avoid certain POI categories** along the route (schools, fire stations,
fuel stations, parking, restaurants, …). fuel stations, parking, restaurants, …).
- **Compute** a route and **draw it** on the map. - **Compute** a route and **draw it** on the map.
- Choose **distance units** (`m` / `km`, or `Auto` — meters under 1 km, - Choose a **unit system** (Metric or Imperial) and **units** (`m`/`km` or
kilometers otherwise) and a **travel mode** (Drive or Walk). `ft`/`mi`, or `Auto` — which uses the small unit for short routes and the
large one for long routes), plus a **travel mode** (Drive or Walk).
- See the route's **distance** and an **estimated travel time** (computed from - See the route's **distance** and an **estimated travel time** (computed from
the mode's average speed). the mode's average speed).

View File

@ -22,8 +22,11 @@
let computing = $state(false); let computing = $state(false);
let picking: 'from' | 'to' | null = $state(null); let picking: 'from' | 'to' | null = $state(null);
// Units for distance output: 'auto' (m if < 1km else km), 'm', or 'km'. // Unit system (Metric or Imperial) and a granular unit override.
let unit = $state<'auto' | 'm' | 'km'>('auto'); // - system 'metric' -> units m / km ; Auto = meters if < 1 km else km
// - system 'imperial'-> units ft / mi ; Auto = feet if < 1/10 mi else mi
let system = $state<'metric' | 'imperial'>('metric');
let unit = $state<'auto' | 'm' | 'km' | 'ft' | 'mi'>('auto');
// Travel-time mode: average speed (km/h) used for the estimate. // Travel-time mode: average speed (km/h) used for the estimate.
let mode = $state<'drive' | 'walk'>('drive'); let mode = $state<'drive' | 'walk'>('drive');
const SPEEDS = { drive: 45, walk: 4.8 }; // km/h const SPEEDS = { drive: 45, walk: 4.8 }; // km/h
@ -40,16 +43,52 @@
]; ];
const selected = $state<Record<string, boolean>>({}); const selected = $state<Record<string, boolean>>({});
// Format distance in meters/km according to the selected unit. // Format a metric distance (meters) into the chosen unit system.
function formatDistance(m: number): string { function formatDistance(m: number): string {
if (unit === 'm' || (unit === 'auto' && m < 1000)) { // Determine effective unit.
if (system === 'imperial') {
const ft = m * 3.28084;
const mi = m / 1609.344;
const useFt = unit === 'ft' || (unit === 'auto' && ft < 528);
if (useFt) {
const v = ft < 10 ? ft.toFixed(1) : Math.round(ft).toString();
return `${v} ft`;
}
// miles
return mi < 10 ? `${mi.toFixed(2)} mi` : `${mi.toFixed(1)} mi`;
}
// metric
const useM = unit === 'm' || (unit === 'auto' && m < 1000);
if (useM) {
const v = m < 10 ? m.toFixed(1) : Math.round(m).toString(); const v = m < 10 ? m.toFixed(1) : Math.round(m).toString();
return `${v} m`; return `${v} m`;
} }
// km // km
const km = m / 1000; const km = m / 1000;
const v = km < 10 ? km.toFixed(2) : km.toFixed(1); return km < 10 ? `${km.toFixed(2)} km` : `${km.toFixed(1)} km`;
return `${v} km`; }
// Show the alternate unit alongside the chosen one (e.g. "3.31 mi (5.33 km)").
function formatAlt(m: number): string | null {
if (unit !== 'auto') {
// mirror in the other system once
if (system === 'metric') {
if (unit === 'm') return `${(m / 1000).toFixed(2)} km`;
if (unit === 'km') return m < 1000 ? `${Math.round(m)} m` : null;
return null;
} else {
if (unit === 'ft') return `${(m / 1609.344).toFixed(2)} mi`;
if (unit === 'mi') return ftAlt(m);
return null;
}
}
// Auto: show the other cheaply for long routes
if (system === 'metric') return m >= 1000 ? `${Math.round(m)} m` : null;
return m >= 160.934 ? `${Math.round(m * 3.28084)} ft` : null;
}
function ftAlt(m: number): string | null {
const ft = m * 3.28084;
return ft >= 528 ? null : `${Math.round(ft)} ft`;
} }
// Estimate travel time (minutes) from distance + mode speed. // Estimate travel time (minutes) from distance + mode speed.
@ -174,13 +213,25 @@
{/each} {/each}
</div> </div>
<div class="row2"> <div class="row3">
<div class="field">
<label for="dir-system">System</label>
<select id="dir-system" bind:value={system}>
<option value="metric">Metric</option>
<option value="imperial">Imperial</option>
</select>
</div>
<div class="field"> <div class="field">
<label for="dir-unit">Units</label> <label for="dir-unit">Units</label>
<select id="dir-unit" bind:value={unit}> <select id="dir-unit" bind:value={unit}>
<option value="auto">Auto</option> <option value="auto">Auto</option>
<option value="m">m</option> {#if system === 'metric'}
<option value="km">km</option> <option value="m">m</option>
<option value="km">km</option>
{:else}
<option value="ft">ft</option>
<option value="mi">mi</option>
{/if}
</select> </select>
</div> </div>
<div class="field"> <div class="field">
@ -205,9 +256,7 @@
<span class="lab">Distance</span> <span class="lab">Distance</span>
<strong>{formatDistance(result.distanceM)}</strong> <strong>{formatDistance(result.distanceM)}</strong>
</div> </div>
{#if result.distanceM >= 1000 && unit === 'km'} {#if formatAlt(result.distanceM)}<div class="mute">{formatAlt(result.distanceM)}</div>{/if}
<div class="mute">({Math.round(result.distanceM)} m)</div>
{/if}
<div class="resline"> <div class="resline">
<span class="lab">Est. time</span> <span class="lab">Est. time</span>
<strong>{formatTime(result.distanceM)}</strong> <strong>{formatTime(result.distanceM)}</strong>
@ -225,7 +274,7 @@
</div> </div>
<style> <style>
.dirs { background: #fff; border-radius: 10px; box-shadow: 0 4px 16px rgba(0,0,0,.25); padding: 12px; width: 260px; font: 0.8rem system-ui, sans-serif; color: #0f172a; } .dirs { background: #fff; border-radius: 10px; box-shadow: 0 4px 16px rgba(0,0,0,.25); padding: 12px; width: 268px; font: 0.8rem system-ui, sans-serif; color: #0f172a; }
.dir-head { font-weight: 700; margin-bottom: 8px; } .dir-head { font-weight: 700; margin-bottom: 8px; }
.point { padding: 6px 8px; background: #f1f5f9; border-radius: 6px; margin: 4px 0; display: flex; align-items: center; gap: 6px; } .point { padding: 6px 8px; background: #f1f5f9; border-radius: 6px; margin: 4px 0; display: flex; align-items: center; gap: 6px; }
.dot { width: 10px; height: 10px; border-radius: 50%; } .dot { width: 10px; height: 10px; border-radius: 50%; }
@ -238,10 +287,10 @@
.avoid { margin: 8px 0; display: grid; gap: 3px; } .avoid { margin: 8px 0; display: grid; gap: 3px; }
.avoid strong { display: block; margin-bottom: 2px; } .avoid strong { display: block; margin-bottom: 2px; }
.avoid label { display: flex; align-items: center; gap: 6px; } .avoid label { display: flex; align-items: center; gap: 6px; }
.row2 { display: flex; gap: 8px; margin: 8px 0; } .row3 { display: flex; gap: 6px; margin: 8px 0; }
.field { flex: 1; } .field { flex: 1; }
.field label { display: block; font-size: 0.7rem; color: #64748b; margin-bottom: 2px; } .field label { display: block; font-size: 0.7rem; color: #64748b; margin-bottom: 2px; }
.field select { width: 100%; padding: 5px; border-radius: 6px; border: 1px solid #cbd5e1; background: #fff; font-size: 0.8rem; } .field select { width: 100%; padding: 5px; border-radius: 6px; border: 1px solid #cbd5e1; background: #fff; font-size: 0.78rem; }
.res { background: #ecfdf5; border: 1px solid #a7f3d0; padding: 8px; border-radius: 6px; margin-top: 6px; } .res { background: #ecfdf5; border: 1px solid #a7f3d0; padding: 8px; border-radius: 6px; margin-top: 6px; }
.resline { display: flex; justify-content: space-between; align-items: baseline; margin: 2px 0; } .resline { display: flex; justify-content: space-between; align-items: baseline; margin: 2px 0; }
.resline .lab { color: #475569; } .resline .lab { color: #475569; }