Яаралтай дуудлага
Дуудлага өгсөн хүний хэлснийг бичнэ эсвэл ярина. Газрыг диспетчер сонгоно, итгэл том үсгээр, баазаас очих зам.
Код
html
<!doctype html>
<!--
dock sample app: emergency dispatcher. The caller describes the place as they say it ("Сансар 24", a shop
name, a building code); the dispatcher types it or speaks it (the mic shows only where the browser has speech input).
dock answers up to three places, never picks one: the dispatcher taps the one the caller confirms on the call.
Then the card: confidence large, the entrance, the instructions, the gate photo slot, the route from the base.
Nothing is sent until the dispatcher presses "Илгээх"; the call card JSON is printed for your dispatch system.
Anything without a matched official address says "албан бус". A code with no door stays not found, never guessed.
Serve sdk/js over http (npm run build, then e.g. `npx serve .`) and open /examples/emergency.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: 15px system-ui, sans-serif; }
body { display: grid; grid-template-columns: 440px 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: 70px; }
button { font: inherit; padding: 6px 12px; margin: 6px 6px 0 0; }
ul { list-style: none; margin: 8px 0; padding: 0; }
li button { width: 100%; text-align: left; margin: 2px 0; }
#conf { font-size: 44px; font-weight: 700; padding: 6px 10px; border-radius: 8px; background: #dcfce7; }
#conf.low { background: #fee2e2; color: #991b1b; }
#photo { border: 1px dashed #aaa; padding: 10px; color: #555; margin: 8px 0; }
pre { background: #f6f6f6; padding: 8px; overflow: auto; font-size: 12px; }
[hidden] { display: none !important; }
</style>
</head>
<body>
<aside>
<form id="ask">
<label>Бааз (урт,өргөрөг)<input name="base" value="106.9177,47.9186"></label>
<label>Дуудлага өгсөн хүний хэлсэн газар<textarea name="said"></textarea></label>
<button>Хайх</button><button id="mic" type="button" hidden>Ярих</button>
</form>
<p id="msg"></p>
<ul id="hits"></ul>
<section id="card" hidden>
<div id="conf"></div>
<p id="where"></p>
<p id="how"></p>
<div id="photo"></div>
<p id="trip"></p>
<button id="send">Илгээх</button>
</section>
<pre id="out" hidden></pre>
</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 = createMap(client, { container: 'map', zoom: 13 });
map.addControl(new maplibregl.NavigationControl());
const $ = (id) => document.getElementById(id);
const form = $('ask');
let markers = [], picked = null, hits = [];
// Speech input where the browser has it (Chrome sends the audio to its own service). Mongolian first.
const Speech = window.SpeechRecognition ?? window.webkitSpeechRecognition;
if (Speech) {
$('mic').hidden = false;
$('mic').onclick = () => {
const rec = new Speech();
rec.lang = 'mn-MN';
rec.onresult = (e) => { form.said.value = e.results[0][0].transcript; form.requestSubmit(); };
rec.start();
};
}
// A building code (plus an optional flat) gives one door or none; words give up to three places, for a person to pick.
async function find(text) {
const [code, ...unit] = text.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, unofficial: door.unofficial, door }];
} catch (e) {
if (e instanceof DockError && (e.code === 'not_found' || e.code === 'retired_code')) return [];
if (!(e instanceof DockError && e.code === 'invalid_code')) throw e;
}
}
const { results } = await client.geocode(text, { limit: 3 });
return results.map((p) => ({ end: p.code ?? p.point, point: p.point, text: label(p), confidence: p.confidence, unofficial: p.unofficial, door: null }));
}
const base = () => form.base.value.split(',').map(Number);
function draw(coords) {
const data = { type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: coords } };
if (map.getSource('run')) return map.getSource('run').setData(data);
map.addSource('run', { type: 'geojson', data });
map.addLayer({ id: 'run', type: 'line', source: 'run', paint: { 'line-color': '#b91c1c', 'line-width': 5 } });
}
form.addEventListener('submit', async (ev) => {
ev.preventDefault();
const said = form.said.value.trim();
$('hits').replaceChildren();
$('card').hidden = $('out').hidden = true;
markers.forEach((m) => m.remove());
markers = [];
picked = null;
if (!said) return;
$('msg').textContent = 'Хайж байна...';
try {
hits = await find(said);
} catch (e) {
$('msg').textContent = e instanceof DockError ? `${e.code}: ${e.message}` : String(e);
return;
}
if (!hits.length) { $('msg').textContent = `"${said}": олдсонгүй. Дуудлага өгсөн хүнээс дахин асуу.`; return; }
$('msg').textContent = 'Дуудлага өгсөн хүнээр шалгуулж, нэгийг сонго:';
for (const [i, h] of hits.entries()) {
const b = document.createElement('button');
b.type = 'button';
b.textContent = `${i + 1}. ${h.text}`;
b.onclick = () => pick(i);
const li = Object.assign(document.createElement('li'), { id: `hit${i}` });
li.append(b);
$('hits').append(li);
markers.push(new maplibregl.Marker({ color: '#6b7280' }).setLngLat(h.point).addTo(map));
}
const bounds = new maplibregl.LngLatBounds(hits[0].point, hits[0].point);
hits.forEach((h) => bounds.extend(h.point));
map.fitBounds(bounds, { padding: 80, maxZoom: 16 });
});
async function pick(i) {
const h = (picked = hits[i]);
$('out').hidden = true;
$('card').hidden = false;
const c = h.confidence;
$('conf').textContent = c === null || c === undefined ? 'Итгэл: тодорхойгүй' : `Итгэл: ${c}/4`;
$('conf').className = c === null || c === undefined || c < 2 ? 'low' : '';
$('where').textContent = h.text;
const d = h.door;
$('how').textContent = d ? [d.entrance.letter && `${d.entrance.letter}-р хаалга`, d.unit?.floor != null && `${d.unit.floor} давхар`,
d.instructions_mn].filter(Boolean).join('. ') : 'Хаалга тодорхойгүй: дуудлага өгсөн хүнээс хаалгаа асуу.';
// dock serves a gate photo only to the org that holds a name for the door (doorPhoto), never by code to others.
$('photo').textContent = 'Хаалганы зураг: алга. Ирэхдээ дуудлага өгсөн хүнээс хаалгаа тайлбарлуул.';
$('trip').textContent = 'Зам тооцож байна...';
markers.push(new maplibregl.Marker({ color: '#b91c1c' }).setLngLat(h.point).addTo(map));
try {
const r = await client.route(base(), h.end, { mode: 'driving' });
if (picked !== h) return;
draw(r.geometry.coordinates);
map.fitBounds(new maplibregl.LngLatBounds(base(), base()).extend(h.point), { padding: 80, maxZoom: 16 });
picked.route = { distance_m: Math.round(r.distance_m), duration_s: Math.round(r.duration_s) };
$('trip').textContent = `Баазаас ${(r.distance_m / 1000).toFixed(1)} км, ойролцоогоор ${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;
$('trip').textContent = 'Баазаас зам олдсонгүй.';
}
}
// The human in the loop: the call card leaves only on the dispatcher's press, with what was picked and from how many.
$('send').onclick = () => {
if (!picked) return;
const card = {
said: form.said.value.trim(), picked_by: 'dispatcher', picked_of: hits.length, label_mn: picked.text,
code: picked.door?.code ?? null, point: picked.point, confidence: picked.confidence ?? null,
unofficial: picked.unofficial, route: picked.route ?? null,
};
$('out').textContent = JSON.stringify(card, null, 2);
$('out').hidden = false;
};
</script>
</body>
</html>