rspace-online/modules/trips/components/folk-route-planner.ts

532 lines
22 KiB
TypeScript

import type { GeoPoint, FittedRoute, RouteInput, IntersectionPoint, ConicArc } from "../lib/types";
import { createProjection, type Projection } from "../lib/projection";
import { fitRoute, findAllIntersections, bezierToSVGCubic, sampleConicCurve, conicTangentAt } from "../lib/conic-math";
// Monaco demo routes that cross each other
const DEMO_ROUTES: [RouteInput, RouteInput] = [
{
label: "Route A",
start: { lng: 7.4267, lat: 43.7396 }, // Casino Monte-Carlo
end: { lng: 7.4163, lat: 43.7275 }, // Fontvieille
},
{
label: "Route B",
start: { lng: 7.4317, lat: 43.7519 }, // Larvotto Beach area
end: { lng: 7.4197, lat: 43.7311 }, // Monaco-Ville / Palace
},
];
// Distinct shades per arc segment
const COLORS_A = ["#00dcff", "#00b8d9", "#0099b3", "#007a8c"];
const COLORS_B = ["#ffb400", "#e6a200", "#cc9000", "#b37e00"];
class FolkRoutePlanner extends HTMLElement {
private shadow: ShadowRoot;
private routeA: FittedRoute | null = null;
private routeB: FittedRoute | null = null;
private intersections: IntersectionPoint[] = [];
private projection: Projection | null = null;
private loading = false;
private error = "";
private inputA: RouteInput = DEMO_ROUTES[0];
private inputB: RouteInput = DEMO_ROUTES[1];
constructor() {
super();
this.shadow = this.attachShadow({ mode: "open" });
}
connectedCallback() {
this.render();
}
private getApiBase(): string {
const path = window.location.pathname;
const parts = path.split("/").filter(Boolean);
if (parts.length >= 2 && parts[1] === "trips") {
return `/${parts[0]}/trips`;
}
return "/demo/trips";
}
private async fetchRoute(input: RouteInput): Promise<FittedRoute> {
const res = await fetch(`${this.getApiBase()}/api/route`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ start: input.start, end: input.end }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({ error: "Unknown error" }));
throw new Error(err.error || `OSRM returned ${res.status}`);
}
const data = await res.json();
if (data.code !== "Ok" || !data.routes?.length) {
throw new Error("No route found between the given points");
}
const route = data.routes[0];
const rawCoords: GeoPoint[] = route.geometry.coordinates.map(
([lng, lat]: [number, number]) => ({ lng, lat }),
);
return {
rawCoords,
projectedPoints: [], // filled after projection
arcs: [],
distance: route.distance,
duration: route.duration,
};
}
private async compute() {
this.loading = true;
this.error = "";
this.render();
try {
// Fetch both routes
const [rawA, rawB] = await Promise.all([
this.fetchRoute(this.inputA),
this.fetchRoute(this.inputB),
]);
// Create unified projection from all coordinates
const allGeo = [...rawA.rawCoords, ...rawB.rawCoords];
this.projection = createProjection(allGeo);
// Project and fit route A
rawA.projectedPoints = rawA.rawCoords.map((g) => this.projection!.toSVG(g));
rawA.arcs = fitRoute(rawA.projectedPoints);
this.routeA = rawA;
// Project and fit route B
rawB.projectedPoints = rawB.rawCoords.map((g) => this.projection!.toSVG(g));
rawB.arcs = fitRoute(rawB.projectedPoints);
this.routeB = rawB;
// Find intersections
this.intersections = findAllIntersections(this.routeA.arcs, this.routeB.arcs);
} catch (e: any) {
this.error = e.message || String(e);
}
this.loading = false;
this.render();
}
private readInputs() {
const q = (sel: string) => this.shadow.querySelector(sel) as HTMLInputElement | null;
const parse = (v: string | undefined, fallback: number) => {
const n = parseFloat(v || "");
return isNaN(n) ? fallback : n;
};
this.inputA = {
label: "Route A",
start: {
lat: parse(q("#a-start-lat")?.value, this.inputA.start.lat),
lng: parse(q("#a-start-lng")?.value, this.inputA.start.lng),
},
end: {
lat: parse(q("#a-end-lat")?.value, this.inputA.end.lat),
lng: parse(q("#a-end-lng")?.value, this.inputA.end.lng),
},
};
this.inputB = {
label: "Route B",
start: {
lat: parse(q("#b-start-lat")?.value, this.inputB.start.lat),
lng: parse(q("#b-start-lng")?.value, this.inputB.start.lng),
},
end: {
lat: parse(q("#b-end-lat")?.value, this.inputB.end.lat),
lng: parse(q("#b-end-lng")?.value, this.inputB.end.lng),
},
};
}
private renderSVG(): string {
if (!this.routeA || !this.routeB || !this.projection) return "";
const vb = this.projection.viewBox;
let svg = `<svg viewBox="${vb}" xmlns="http://www.w3.org/2000/svg" class="route-svg">`;
// Defs: grid + pulse animation
svg += `<defs>
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="rgba(255,255,255,0.05)" stroke-width="0.5"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid)"/>`;
// Layer 1: Grid (done above)
// Layer 2: Overlap zone rectangles
svg += this.renderOverlapZones(this.routeA.arcs, this.routeB.arcs);
// Layer 3: Ghost conic curves (dashed, low opacity)
svg += this.renderGhostCurves(this.routeA.arcs, "A");
svg += this.renderGhostCurves(this.routeB.arcs, "B");
// Layer 4: Route waypoints (subtle)
svg += this.renderWaypoints(this.routeA.projectedPoints, "rgba(0,220,255,0.2)");
svg += this.renderWaypoints(this.routeB.projectedPoints, "rgba(255,180,0,0.2)");
// Layer 5: Fitted arc paths (solid, distinct shades)
svg += this.renderArcs(this.routeA.arcs, COLORS_A, "routeA");
svg += this.renderArcs(this.routeB.arcs, COLORS_B, "routeB");
// Layer 6: Arc labels
svg += this.renderArcLabels(this.routeA.arcs, "A", COLORS_A);
svg += this.renderArcLabels(this.routeB.arcs, "B", COLORS_B);
// Layer 7: Intersection crossing lines
svg += this.renderCrossingLines();
// Layer 8: Pulsing intersection markers + labels
for (let i = 0; i < this.intersections.length; i++) {
const pt = this.intersections[i].point;
svg += `<circle cx="${pt.x.toFixed(1)}" cy="${pt.y.toFixed(1)}" r="8" fill="none" stroke="#ff3355" stroke-width="1.5" class="pulse-ring"/>`;
svg += `<circle cx="${pt.x.toFixed(1)}" cy="${pt.y.toFixed(1)}" r="5" fill="#ff3355" stroke="#fff" stroke-width="1.5" class="pulse-dot"/>`;
svg += `<text x="${(pt.x + 12).toFixed(1)}" y="${(pt.y - 10).toFixed(1)}" fill="#ff3355" font-size="11" font-family="monospace" font-weight="bold">X${i + 1}</text>`;
}
// Layer 9: Start/end markers
svg += this.renderMarker(this.routeA.projectedPoints[0], "A0", "#00dcff");
svg += this.renderMarker(this.routeA.projectedPoints[this.routeA.projectedPoints.length - 1], "A1", "#00dcff");
svg += this.renderMarker(this.routeB.projectedPoints[0], "B0", "#ffb400");
svg += this.renderMarker(this.routeB.projectedPoints[this.routeB.projectedPoints.length - 1], "B1", "#ffb400");
// Legend
svg += this.renderLegend(vb);
svg += `</svg>`;
return svg;
}
private renderOverlapZones(arcsA: ConicArc[], arcsB: ConicArc[]): string {
let svg = "";
for (const aA of arcsA) {
const bbA = this.arcBounds(aA.points);
for (const aB of arcsB) {
const bbB = this.arcBounds(aB.points);
// Compute intersection of bounding boxes
const x1 = Math.max(bbA.minX, bbB.minX);
const y1 = Math.max(bbA.minY, bbB.minY);
const x2 = Math.min(bbA.maxX, bbB.maxX);
const y2 = Math.min(bbA.maxY, bbB.maxY);
if (x2 > x1 && y2 > y1) {
const pad = 8;
svg += `<rect x="${(x1 - pad).toFixed(1)}" y="${(y1 - pad).toFixed(1)}" width="${(x2 - x1 + pad * 2).toFixed(1)}" height="${(y2 - y1 + pad * 2).toFixed(1)}" fill="rgba(255,0,200,0.06)" stroke="rgba(255,0,200,0.15)" stroke-width="0.5" stroke-dasharray="4 3" rx="3"/>`;
}
}
}
return svg;
}
private renderGhostCurves(arcs: ConicArc[], routeId: string): string {
const colors = routeId === "A" ? COLORS_A : COLORS_B;
let svg = "";
for (let i = 0; i < arcs.length; i++) {
const arc = arcs[i];
if (arc.points.length < 2 || arc.type === "degenerate") continue;
const extend = 80;
const sampled = sampleConicCurve(arc.coeffs, arc.points, extend);
if (sampled.length < 2) continue;
let d = `M ${sampled[0].x.toFixed(1)} ${sampled[0].y.toFixed(1)}`;
for (let j = 1; j < sampled.length; j++) {
d += ` L ${sampled[j].x.toFixed(1)} ${sampled[j].y.toFixed(1)}`;
}
const color = colors[i % colors.length];
svg += `<path d="${d}" fill="none" stroke="${color}" stroke-width="1" stroke-dasharray="5 4" opacity="0.25" stroke-linecap="round"/>`;
}
return svg;
}
private renderWaypoints(pts: { x: number; y: number }[], color: string): string {
return pts.map((p) =>
`<circle cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="1.5" fill="${color}"/>`
).join("");
}
private renderArcs(arcs: ConicArc[], colors: string[], id: string): string {
let svg = "";
for (let i = 0; i < arcs.length; i++) {
const arc = arcs[i];
if (arc.beziers.length === 0) continue;
const first = arc.beziers[0].p0;
let d = `M ${first.x.toFixed(1)} ${first.y.toFixed(1)} `;
for (const b of arc.beziers) {
d += bezierToSVGCubic(b) + " ";
}
const color = colors[i % colors.length];
svg += `<path d="${d}" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.85" class="arc-path" data-id="${id}-${i}"/>`;
}
return svg;
}
private renderArcLabels(arcs: ConicArc[], prefix: string, colors: string[]): string {
let svg = "";
for (let i = 0; i < arcs.length; i++) {
const arc = arcs[i];
if (arc.points.length < 2) continue;
// Label at the midpoint of the arc
const midIdx = Math.floor(arc.points.length / 2);
const mp = arc.points[midIdx];
const label = `${prefix}.${i + 1} ${arc.type}`;
const color = colors[i % colors.length];
// Background for readability
svg += `<rect x="${(mp.x + 5).toFixed(1)}" y="${(mp.y - 11).toFixed(1)}" width="${(label.length * 6.2 + 6).toFixed(0)}" height="14" rx="3" fill="rgba(13,13,26,0.8)"/>`;
svg += `<text x="${(mp.x + 8).toFixed(1)}" y="${(mp.y).toFixed(1)}" fill="${color}" font-size="9" font-family="monospace" opacity="0.8">${label}</text>`;
}
return svg;
}
private renderCrossingLines(): string {
if (!this.routeA || !this.routeB) return "";
let svg = "";
const lineLen = 20;
for (let i = 0; i < this.intersections.length; i++) {
const ix = this.intersections[i];
const pt = ix.point;
const arcA = this.routeA.arcs[ix.arcIndexA];
const arcB = this.routeB.arcs[ix.arcIndexB];
if (!arcA || !arcB) continue;
const tA = conicTangentAt(arcA.coeffs, pt);
const tB = conicTangentAt(arcB.coeffs, pt);
// Draw tangent lines
const colorA = COLORS_A[ix.arcIndexA % COLORS_A.length];
const colorB = COLORS_B[ix.arcIndexB % COLORS_B.length];
svg += `<line x1="${(pt.x - tA.dx * lineLen).toFixed(1)}" y1="${(pt.y - tA.dy * lineLen).toFixed(1)}" x2="${(pt.x + tA.dx * lineLen).toFixed(1)}" y2="${(pt.y + tA.dy * lineLen).toFixed(1)}" stroke="${colorA}" stroke-width="1.5" opacity="0.7" stroke-linecap="round"/>`;
svg += `<line x1="${(pt.x - tB.dx * lineLen).toFixed(1)}" y1="${(pt.y - tB.dy * lineLen).toFixed(1)}" x2="${(pt.x + tB.dx * lineLen).toFixed(1)}" y2="${(pt.y + tB.dy * lineLen).toFixed(1)}" stroke="${colorB}" stroke-width="1.5" opacity="0.7" stroke-linecap="round"/>`;
// Compute crossing angle
const dot = Math.abs(tA.dx * tB.dx + tA.dy * tB.dy);
const angle = Math.acos(Math.min(1, dot)) * (180 / Math.PI);
svg += `<text x="${(pt.x + 14).toFixed(1)}" y="${(pt.y + 18).toFixed(1)}" fill="rgba(255,255,255,0.6)" font-size="8" font-family="monospace">${angle.toFixed(1)}\u00B0</text>`;
}
return svg;
}
private renderMarker(pt: { x: number; y: number }, label: string, color: string): string {
return `<circle cx="${pt.x.toFixed(1)}" cy="${pt.y.toFixed(1)}" r="5" fill="${color}" stroke="#1a1a2e" stroke-width="1.5"/>` +
`<text x="${(pt.x + 8).toFixed(1)}" y="${(pt.y + 4).toFixed(1)}" fill="${color}" font-size="10" font-family="monospace" font-weight="bold">${label}</text>`;
}
private renderLegend(viewBox: string): string {
// Parse viewBox to position legend in top-right
const parts = viewBox.split(" ").map(Number);
const vx = parts[0], vy = parts[1], vw = parts[2], vh = parts[3];
const lx = vx + vw - 155;
const ly = vy + 10;
let svg = `<g transform="translate(${lx.toFixed(0)},${ly.toFixed(0)})">`;
svg += `<rect x="0" y="0" width="150" height="78" rx="5" fill="rgba(13,13,26,0.85)" stroke="rgba(255,255,255,0.1)" stroke-width="0.5"/>`;
// Ghost curve
svg += `<line x1="8" y1="14" x2="30" y2="14" stroke="#aaa" stroke-width="1" stroke-dasharray="4 3" opacity="0.5"/>`;
svg += `<text x="36" y="17" fill="rgba(255,255,255,0.5)" font-size="8" font-family="monospace">Ghost conic</text>`;
// Fitted arc
svg += `<line x1="8" y1="28" x2="30" y2="28" stroke="#00dcff" stroke-width="2.5" opacity="0.85"/>`;
svg += `<text x="36" y="31" fill="rgba(255,255,255,0.5)" font-size="8" font-family="monospace">Fitted arc</text>`;
// Overlap zone
svg += `<rect x="8" y="38" width="22" height="10" fill="rgba(255,0,200,0.12)" stroke="rgba(255,0,200,0.3)" stroke-width="0.5" rx="2"/>`;
svg += `<text x="36" y="47" fill="rgba(255,255,255,0.5)" font-size="8" font-family="monospace">Overlap zone</text>`;
// Intersection
svg += `<circle cx="19" cy="63" r="4" fill="#ff3355" stroke="#fff" stroke-width="1" class="pulse-dot"/>`;
svg += `<text x="36" y="66" fill="rgba(255,255,255,0.5)" font-size="8" font-family="monospace">Intersection</text>`;
svg += `</g>`;
return svg;
}
private arcBounds(pts: { x: number; y: number }[]): { minX: number; maxX: number; minY: number; maxY: number } {
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
for (const p of pts) {
if (p.x < minX) minX = p.x;
if (p.x > maxX) maxX = p.x;
if (p.y < minY) minY = p.y;
if (p.y > maxY) maxY = p.y;
}
return { minX, maxX, minY, maxY };
}
private renderInfo(): string {
if (!this.routeA || !this.routeB) return "";
const fmtDist = (m: number) => m >= 1000 ? `${(m / 1000).toFixed(1)}km` : `${Math.round(m)}m`;
const fmtTime = (s: number) => {
const min = Math.floor(s / 60);
return min > 0 ? `${min}m ${Math.round(s % 60)}s` : `${Math.round(s)}s`;
};
let html = `<div class="info-grid">`;
// Summary row
html += `<div class="info-summary">
<span class="tag cyan">Route A: ${this.routeA.arcs.length} arcs | ${fmtDist(this.routeA.distance)} | ${fmtTime(this.routeA.duration)}</span>
<span class="tag amber">Route B: ${this.routeB.arcs.length} arcs | ${fmtDist(this.routeB.distance)} | ${fmtTime(this.routeB.duration)}</span>
<span class="tag red">${this.intersections.length} intersection${this.intersections.length !== 1 ? "s" : ""}</span>
</div>`;
// Arc details
html += `<div class="arc-details"><h4>Route A Arcs</h4>`;
for (let i = 0; i < this.routeA.arcs.length; i++) {
const arc = this.routeA.arcs[i];
html += `<div class="arc-item cyan">A.${i + 1}: <strong>${arc.type}</strong> e=${arc.eccentricity.toFixed(3)}</div>`;
}
html += `</div>`;
html += `<div class="arc-details"><h4>Route B Arcs</h4>`;
for (let i = 0; i < this.routeB.arcs.length; i++) {
const arc = this.routeB.arcs[i];
html += `<div class="arc-item amber">B.${i + 1}: <strong>${arc.type}</strong> e=${arc.eccentricity.toFixed(3)}</div>`;
}
html += `</div>`;
// Intersection details
if (this.intersections.length > 0 && this.projection) {
html += `<div class="arc-details"><h4>Intersections</h4>`;
for (let i = 0; i < this.intersections.length; i++) {
const ix = this.intersections[i];
const geo = this.projection.toGeo(ix.point);
html += `<div class="arc-item red">X${i + 1}: ${geo.lat.toFixed(5)}, ${geo.lng.toFixed(5)} (A.${ix.arcIndexA + 1} / B.${ix.arcIndexB + 1})</div>`;
}
html += `</div>`;
}
html += `</div>`;
return html;
}
private render() {
this.shadow.innerHTML = `
<style>
:host { display: block; font-family: -apple-system, system-ui, sans-serif; color: #e0e0e0; }
.container { background: #1a1a2e; border-radius: 12px; overflow: hidden; }
.header { padding: 16px 20px; border-bottom: 1px solid rgba(255,255,255,0.08); }
.header h2 { margin: 0 0 4px 0; font-size: 18px; color: #fff; }
.header p { margin: 0; font-size: 13px; opacity: 0.6; }
.input-panel { padding: 16px 20px; display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; border-bottom: 1px solid rgba(255,255,255,0.08); }
.route-group { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.route-group label { font-size: 12px; font-weight: 600; min-width: 52px; }
.route-group.a label { color: #00dcff; }
.route-group.b label { color: #ffb400; }
input { background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.12); border-radius: 6px; padding: 6px 8px; color: #e0e0e0; font-size: 12px; font-family: monospace; width: 88px; }
input:focus { outline: none; border-color: rgba(255,255,255,0.3); }
input::placeholder { color: rgba(255,255,255,0.3); }
.btn-row { display: flex; gap: 8px; }
button { background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.15); border-radius: 6px; padding: 6px 14px; color: #e0e0e0; font-size: 12px; cursor: pointer; transition: background 0.15s; }
button:hover { background: rgba(255,255,255,0.14); }
button.primary { background: rgba(0,220,255,0.15); border-color: rgba(0,220,255,0.3); color: #00dcff; }
button.primary:hover { background: rgba(0,220,255,0.25); }
button:disabled { opacity: 0.4; cursor: not-allowed; }
.svg-container { padding: 12px; background: #12121f; }
.route-svg { width: 100%; height: auto; max-height: 500px; border-radius: 8px; background: #0d0d1a; }
.arc-path:hover { stroke-width: 4; opacity: 1; }
@keyframes pulse { 0%,100% { opacity: 0.9; r: 5; } 50% { opacity: 0.5; r: 8; } }
@keyframes pulse-ring { 0%,100% { opacity: 0.6; r: 8; } 50% { opacity: 0; r: 14; } }
.pulse-dot { animation: pulse 2s ease-in-out infinite; }
.pulse-ring { animation: pulse-ring 2s ease-in-out infinite; }
.error { padding: 12px 20px; color: #ff5577; font-size: 13px; }
.loading { padding: 20px; text-align: center; color: rgba(255,255,255,0.5); font-size: 13px; }
.empty { padding: 40px 20px; text-align: center; color: rgba(255,255,255,0.3); font-size: 14px; }
.info-grid { padding: 16px 20px; }
.info-summary { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
.tag { padding: 4px 10px; border-radius: 12px; font-size: 12px; font-family: monospace; }
.tag.cyan { background: rgba(0,220,255,0.12); color: #00dcff; }
.tag.amber { background: rgba(255,180,0,0.12); color: #ffb400; }
.tag.red { background: rgba(255,51,85,0.12); color: #ff3355; }
.arc-details { margin-bottom: 8px; }
.arc-details h4 { margin: 0 0 4px 0; font-size: 12px; opacity: 0.5; text-transform: uppercase; letter-spacing: 0.5px; }
.arc-item { font-size: 12px; font-family: monospace; padding: 2px 0; }
.arc-item.cyan { color: rgba(0,220,255,0.7); }
.arc-item.amber { color: rgba(255,180,0,0.7); }
.arc-item.red { color: rgba(255,51,85,0.8); }
.math-note { padding: 12px 20px; border-top: 1px solid rgba(255,255,255,0.05); font-size: 11px; opacity: 0.4; font-family: monospace; }
</style>
<div class="container">
<div class="header">
<h2>rTrips — Route Planner</h2>
<p>Plan paths between destinations. Fit conic arcs and find where routes intersect.</p>
</div>
<div class="input-panel">
<div class="route-group a">
<label>Route A</label>
<input id="a-start-lat" value="${this.inputA.start.lat}" placeholder="lat" title="Start latitude">
<input id="a-start-lng" value="${this.inputA.start.lng}" placeholder="lng" title="Start longitude">
<span style="opacity:0.3">to</span>
<input id="a-end-lat" value="${this.inputA.end.lat}" placeholder="lat" title="End latitude">
<input id="a-end-lng" value="${this.inputA.end.lng}" placeholder="lng" title="End longitude">
</div>
<div class="route-group b">
<label>Route B</label>
<input id="b-start-lat" value="${this.inputB.start.lat}" placeholder="lat" title="Start latitude">
<input id="b-start-lng" value="${this.inputB.start.lng}" placeholder="lng" title="Start longitude">
<span style="opacity:0.3">to</span>
<input id="b-end-lat" value="${this.inputB.end.lat}" placeholder="lat" title="End latitude">
<input id="b-end-lng" value="${this.inputB.end.lng}" placeholder="lng" title="End longitude">
</div>
<div class="btn-row">
<button class="primary" id="btn-compute" ${this.loading ? "disabled" : ""}>Compute</button>
<button id="btn-demo">Use Demo</button>
</div>
</div>
${this.error ? `<div class="error">${this.error}</div>` : ""}
${this.loading ? `<div class="loading">Fetching routes and computing conic fits...</div>` : ""}
${this.routeA && this.routeB ? `
<div class="svg-container">${this.renderSVG()}</div>
${this.renderInfo()}
<div class="math-note">
Conic: Ax\u00B2+Bxy+Cy\u00B2+Dx+Ey+F=0 | Discriminant \u0394=B\u00B2\u22124AC |
Intersection via Sylvester resultant \u2192 degree-4 polynomial \u2192 companion matrix eigenvalues
</div>
` : ""}
${!this.routeA && !this.routeB && !this.loading && !this.error ? `
<div class="empty">
Enter start/end coordinates for two routes, or click <strong>Use Demo</strong> to load Monaco routes.
</div>
` : ""}
</div>`;
// Attach event listeners
this.shadow.querySelector("#btn-compute")?.addEventListener("click", () => {
this.readInputs();
this.compute();
});
this.shadow.querySelector("#btn-demo")?.addEventListener("click", () => {
this.inputA = DEMO_ROUTES[0];
this.inputB = DEMO_ROUTES[1];
this.compute();
});
}
static define() {
if (!customElements.get("folk-route-planner")) {
customElements.define("folk-route-planner", FolkRoutePlanner);
}
}
}
FolkRoutePlanner.define();
export { FolkRoutePlanner };