Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/app/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,8 @@ var legAltitudeMap = null; // null = not loaded yet (or last fetch failed —
var legAltitudePointIds = null; // Set of endpoint ids from the same file.
var legAltitudeDataset = null; // Raw validated dataset for Charts copy/view.
var legAltitudeDirectionPool = null; // Directed altitude entries, one per allowed direction.
var routeTemplates = null; // null = not loaded yet; [] or populated from route-templates.json.
var templateLegSuggestions = []; // Canvas hit boxes for route-template waypoint chips.
var showDrift = true; // 10-degree drift reference lines
var showWpNames = true; // draw waypoint names (off = empty circle)
var wpNameAngle = 0; // waypoint-name rotation: 0 / 90 / 180 / 270 deg
Expand Down
55 changes: 55 additions & 0 deletions docs/app/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -1325,6 +1325,7 @@ window.normalizeCommChangeSuppressions = normalizeCommChangeSuppressions;

function drawLegs() {
const zoomScale = legZoomScale();
templateLegSuggestions = [];

// Pre-compute cumulative outbound times (walk legs in reverse so each
// entry is "total return time from the last waypoint through leg i").
Expand Down Expand Up @@ -1443,10 +1444,64 @@ function drawLegs() {
cumOutArr[i], tune('inkColor'), 'rgba(255,204,214,0.80)', zoomScale);
}
}
drawTemplateLegSuggestions(i, mid.x, mid.y, dx, dy, nx, ny, zoomScale);
if (showMidLeg) drawDistanceBadge(mid.x, mid.y, dist);
}
}

function drawTemplateLegSuggestions(legIndex, midX, midY, dx, dy, nx, ny, zoomScale) {
if (typeof routeTemplateSuggestionsForLeg !== 'function') return;
const suggestions = routeTemplateSuggestionsForLeg(legIndex);
if (!suggestions.length) return;

const fontPx = Math.max(11, Math.round(12 * zoomScale));
const padX = 7;
const h = fontPx + 9;
const gap = 4;
const labels = suggestions.map(s => String(s.label || s.name || '').trim()).filter(Boolean);
if (!labels.length) return;

octx.save();
octx.font = `bold ${fontPx}px sans-serif`;
const widths = labels.map(label => Math.ceil(octx.measureText(label).width) + padX * 2);
const totalW = widths.reduce((sum, w) => sum + w, 0) + gap * (widths.length - 1);
const perp = legDefaultLabelPerp(1) + h + 10;
let x = midX - totalW / 2;
const y = midY + ny * perp - h / 2;
const alongAdjust = (dy < -0.2 ? 1 : 0) * (h + 2);
x += dx * alongAdjust;
const baseY = y + dy * alongAdjust;

octx.textAlign = 'center';
octx.textBaseline = 'middle';
for (let j = 0; j < labels.length; j++) {
const w = widths[j];
const bx = x;
const by = baseY;
octx.fillStyle = 'rgba(42, 36, 29, 0.92)';
octx.strokeStyle = 'rgba(255, 214, 93, 0.92)';
octx.lineWidth = 1.5;
octx.beginPath();
if (typeof octx.roundRect === 'function') octx.roundRect(bx, by, w, h, 5);
else octx.rect(bx, by, w, h);
octx.fill();
octx.stroke();
octx.fillStyle = '#fff2a8';
octx.fillText(labels[j], bx + w / 2, by + h / 2 + 0.5);
templateLegSuggestions.push({
legIndex,
name: suggestions[j].name,
templateId: suggestions[j].templateId || '',
x: bx,
y: by,
w,
h,
});
x += w + gap;
}
octx.restore();
}

// Drift reference lines, one from each end, defaulting to half the leg length.
function drawDriftLines(sa, sb) {
const a = driftAngleRad();
Expand Down
107 changes: 107 additions & 0 deletions docs/app/interact.js
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,106 @@ function splitLegAt(legIndex, latlng) {
}
window.splitLegAt = splitLegAt;

function routeTemplateWaypointKey(wp) {
const raw = String(wp && wp.name || '').trim();
if (!raw) return '';
if (typeof findNavWpToken === 'function') {
const ref = findNavWpToken(raw);
if (ref && ref.name) return String(ref.name).toUpperCase();
}
const canonical = typeof canonicalNavWaypointName === 'function'
? canonicalNavWaypointName(raw) : raw;
return String(canonical || raw).trim().toUpperCase();
}

function routeTemplateSuggestionsForLeg(legIndex) {
if (!showNavWP || !Array.isArray(routeTemplates) || !routeTemplates.length) return [];
const i = Number(legIndex);
if (!Number.isInteger(i) || i < 0 || i >= state.legs.length) return [];
const from = routeTemplateWaypointKey(state.waypoints[i]);
const to = routeTemplateWaypointKey(state.waypoints[i + 1]);
if (!from || !to || from === to) return [];

const alreadyInRoute = new Set(state.waypoints.map(routeTemplateWaypointKey).filter(Boolean));
const out = [];
const seen = new Set();
const paths = [];
for (const template of routeTemplates) {
const points = Array.isArray(template && template.waypoints)
? template.waypoints.map(p => String(p || '').trim().toUpperCase()).filter(Boolean)
: [];
for (let start = 0; start < points.length - 2; start++) {
if (points[start] !== from) continue;
for (let end = start + 2; end < points.length; end++) {
if (points[end] === to) {
paths.push({
templateId: template.id || '',
missing: points.slice(start + 1, end),
});
break;
}
}
}
}
paths.sort((a, b) => a.missing.length - b.missing.length);
for (const path of paths) {
for (const name of path.missing) {
if (alreadyInRoute.has(name) || seen.has(name)) continue;
seen.add(name);
out.push({
legIndex: i,
name,
templateId: path.templateId,
label: typeof navName === 'function' ? (navName(name) || name) : name,
});
if (out.length >= 6) return out;
}
}
return out;
}
window.routeTemplateSuggestionsForLeg = routeTemplateSuggestionsForLeg;

function insertRouteTemplateSuggestion(legIndex, name) {
const i = Number(legIndex);
if (!Number.isInteger(i) || i < 0 || i >= state.legs.length) return false;
if (!state.waypoints[i] || !state.waypoints[i + 1]) return false;
if (typeof findNavWpToken !== 'function') return false;
const point = findNavWpToken(name);
if (!point || !Number.isFinite(point.lat) || !Number.isFinite(point.lng)) return false;

const source = state.legs[i];
const inserted = { lat: r5(point.lat), lng: r5(point.lng), name: point.name };
const first = splitLegCopy(source);
const second = splitLegCopy(source);
if (source && source._legAltitudeAuto) {
first._legAltitudeAuto = 1;
second._legAltitudeAuto = 1;
}
state.waypoints.splice(i + 1, 0, inserted);
state.legs.splice(i, 1, first, second);
if (state.legs.length !== Math.max(0, state.waypoints.length - 1)) syncLegs();
if (source && source._legAltitudeAuto &&
typeof applyLegAltitudesToRoute === 'function') {
applyLegAltitudesToRoute();
}
if (typeof seedCommChangeNotes === 'function') seedCommChangeNotes();
state.selected = { type: 'wp', index: i + 1 };
draw();
showInspector();
return true;
}
window.insertRouteTemplateSuggestion = insertRouteTemplateSuggestion;

function hitRouteTemplateSuggestion(px, py) {
if (!Array.isArray(templateLegSuggestions)) return null;
for (let i = templateLegSuggestions.length - 1; i >= 0; i--) {
const s = templateLegSuggestions[i];
if (px >= s.x && px <= s.x + s.w && py >= s.y && py <= s.y + s.h) return s;
}
return null;
}
window.hitRouteTemplateSuggestion = hitRouteTemplateSuggestion;

function deleteSelectedWpOrNote() {
if (state.selected.type === 'wp') {
deleteWaypoint(state.selected.index);
Expand Down Expand Up @@ -1771,6 +1871,12 @@ map.on('mousedown', e => {
showInspector(); draw();
return;
}
const suggestion = hitRouteTemplateSuggestion(p.x, p.y);
if (suggestion) {
downHit = true;
insertRouteTemplateSuggestion(suggestion.legIndex, suggestion.name);
return;
}
const cum = hitCumLabel(p.x, p.y);
if (cum) {
downHit = true;
Expand Down Expand Up @@ -2102,6 +2208,7 @@ mapEl.addEventListener('dblclick', e => {
const p = { x: e.clientX - rect.left, y: e.clientY - rect.top };
if (hitNote(p.x, p.y) >= 0) return;
if (hitWaypointCandidates(p.x, p.y).length) return;
if (hitRouteTemplateSuggestion(p.x, p.y)) return;
if (hitCumLabel(p.x, p.y) || hitCumLabelRet(p.x, p.y) || hitLegLabel(p.x, p.y)) return;
if (hitOverlayMarkerCandidates(p.x, p.y).length) return;
const leg = hitLeg(p.x, p.y);
Expand Down
5 changes: 3 additions & 2 deletions docs/app/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -518,8 +518,6 @@ async function buildRouteFromQuery(raw) {
return true;
}

let routeTemplates = null;

function routeTemplateLabel(template) {
const lang = (window.__navLang || document.documentElement.lang || '').toLowerCase();
if (lang.slice(0, 2) === 'he' && template.he) return template.he;
Expand Down Expand Up @@ -2030,6 +2028,9 @@ loadLegAltitudes().then(() => {
if (state.selected) showInspector();
}
});
loadRouteTemplates().then(() => {
draw();
});
// Comm-change dataset (issue #399): parallel fetch so the rings appear
// on first paint and the inspector badge is available immediately for
// a selection restored from sessionStorage. Rings draw independently of
Expand Down
Loading
Loading