Delivery dispatch
Orders one per line, as codes or addresses. A route from the depot in the order given. A line with no match stays not found.
Code
html
<!doctype html>
<!--
dock sample app: delivery dispatch. Paste today's orders, one per line, as customers wrote them: a building code
("CDFGH 17") or an address ("Сансар 24"). Each line is resolved and routed from the depot in the order
given (no reordering). Lines that resolve to nothing stay in the list, marked, so nobody drops an order.
Serve sdk/js over http (npm run build, then e.g. `npx serve .`) and open /examples/dispatch.html#key=dk_...
The key needs door:read, geocode:read, route:read and tiles:read; a free key has them.
-->
<html lang="mn">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>dock: хүргэлтийн диспетчер</title>
<link href="https://dock.mn/sdk/v1/dock.css" rel="stylesheet">
<script src="https://dock.mn/sdk/v1/dock.js"></script>
<style>
html, body { margin: 0; height: 100%; font: 14px system-ui, sans-serif; }
body { display: grid; grid-template-columns: 420px 1fr; }
aside { padding: 12px; overflow: auto; border-right: 1px solid #ddd; }
#map { height: 100%; }
textarea, input { width: 100%; box-sizing: border-box; padding: 6px; font: inherit; }
textarea { height: 120px; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
td, th { padding: 4px; border-bottom: 1px solid #eee; text-align: left; vertical-align: top; }
tr.miss td { color: #a33; }
.n { display: inline-block; width: 20px; height: 20px; border-radius: 50%; background: #1d4ed8; color: #fff; text-align: center; line-height: 20px; font-size: 12px; }
</style>
</head>
<body>
<aside>
<form id="plan">
<label>Агуулах (код, хаяг эсвэл урт,өргөрөг)<input name="depot" value="106.9177,47.9186"></label>
<label>Захиалгууд, мөр бүрт нэг<textarea name="orders">Улсын их дэлгүүр
CDFGH 17
Сансар Төв</textarea></label>
<button>Төлөвлөх</button>
</form>
<p id="sum"></p>
<table><thead><tr><th>#</th><th>Хаана</th><th>км</th><th>мин</th></tr></thead><tbody id="stops"></tbody></table>
</aside>
<div id="map"></div>
<script type="module">
const { maplibregl } = dock;
const { Client, DockError, createMap, label } = dock;
const params = new URLSearchParams(location.hash.slice(1));
const client = new Client({ key: 'dk_...' });
const map = new dock.Map({ client, ui: 'sheet', container: 'map', zoom: 13 });
map.addControl(new maplibregl.NavigationControl());
let markers = [];
// A line is a building code (plus an optional flat), "lon,lat", or words for geocode. Returns null when nothing matched.
async function resolve(line) {
const m = line.match(/^\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*$/);
if (m) return { end: [Number(m[1]), Number(m[2])], point: [Number(m[1]), Number(m[2])], text: line.trim(), confidence: null };
const [code, ...unit] = line.trim().split(/\s+/);
if (code.length === 5) {
try {
const door = await client.door(code, { unit: unit.join(' ') || undefined });
return { end: door.code, point: door.point, text: label(door), confidence: door.confidence, door };
} catch (e) {
// A real code with no door is a miss; never guess another place for it. Not a code: try it as words.
if (e instanceof DockError && (e.code === 'not_found' || e.code === 'retired_code')) return null;
if (!(e instanceof DockError && e.code === 'invalid_code')) throw e;
}
}
const { results } = await client.geocode(line, { limit: 1 });
const p = results[0];
return p ? { end: p.code ?? p.point, point: p.point, text: label(p), confidence: p.confidence } : null;
}
const cell = (tr, text) => { const td = tr.insertCell(); td.textContent = text; return td; };
const conf = (c) => (c === null || c === undefined ? 'итгэл тодорхойгүй' : `итгэл ${c}/4`);
function drawLine(id, coords) {
const data = { type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: coords } };
if (map.getSource(id)) return map.getSource(id).setData(data);
map.addSource(id, { type: 'geojson', data });
map.addLayer({ id, type: 'line', source: id, paint: { 'line-color': '#1d4ed8', 'line-width': 4, 'line-opacity': 0.8 } });
}
document.getElementById('plan').addEventListener('submit', async (ev) => {
ev.preventDefault();
const form = new FormData(ev.target);
const lines = String(form.get('orders')).split('\n').map((l) => l.trim()).filter(Boolean);
const body = document.getElementById('stops');
const sum = document.getElementById('sum');
body.replaceChildren();
markers.forEach((m) => m.remove());
markers = [];
sum.textContent = 'Тооцоолж байна...';
try {
const depot = await resolve(String(form.get('depot')));
if (!depot) { sum.textContent = 'Агуулах олдсонгүй'; return; }
markers.push(new maplibregl.Marker({ color: '#111' }).setLngLat(depot.point).addTo(map));
const line = [];
const bounds = new maplibregl.LngLatBounds(depot.point, depot.point);
let from = depot.end, km = 0, min = 0, missed = 0;
for (const [i, text] of lines.entries()) {
const tr = body.insertRow();
cell(tr, String(i + 1));
const stop = await resolve(text);
if (!stop) {
tr.className = 'miss';
cell(tr, `${text}: олдсонгүй, хэрэглэгчээс асуу`);
cell(tr, ''); cell(tr, '');
missed++;
continue;
}
const where = cell(tr, stop.text);
const sub = document.createElement('div');
sub.textContent = [conf(stop.confidence), stop.door?.entrance.letter && `хаалга ${stop.door.entrance.letter}`,
stop.door?.unit?.floor != null && `${stop.door.unit.floor} давхар`].filter(Boolean).join(', ');
where.append(sub);
const el = document.createElement('span');
el.className = 'n';
el.textContent = String(i + 1);
markers.push(new maplibregl.Marker({ element: el }).setLngLat(stop.point).addTo(map));
bounds.extend(stop.point);
try {
const r = await client.route(from, stop.end, { mode: 'driving' });
line.push(...r.geometry.coordinates);
km += r.distance_m / 1000;
min += r.duration_s / 60;
cell(tr, (r.distance_m / 1000).toFixed(1));
cell(tr, Math.round(r.duration_s / 60) + (r.to.gap_m > 30 ? ` + ${Math.round(r.to.gap_m)} м явган` : ''));
} catch (e) {
if (!(e instanceof DockError && e.code === 'no_route')) throw e;
cell(tr, 'зам алга'); cell(tr, '');
}
from = stop.end;
}
drawLine('dispatch-route', line);
map.fitBounds(bounds, { padding: 60, maxZoom: 16 });
sum.textContent = `${lines.length - missed} цэг, ${km.toFixed(1)} км, ${Math.round(min)} мин, ` +
`олдоогүй ${missed}. Түгжрэл тооцоогүй.`;
} catch (e) {
sum.textContent = e instanceof DockError ? `${e.code}: ${e.message}` : String(e);
}
});
</script>
</body>
</html>