Зочны хуудас
Буудлын хаалга монгол, англи, япон, солонгос, хятад, орос хэлээр. Жолоочид үзүүлэх карт, алхах зам, кодтой бол QR.
Код
html
<!doctype html>
<!--
dock sample app: hotel guest page. One page per hotel, sent to guests before they arrive: the entrance on a map,
the page in mn, en, ja, ko, zh or ru (#lang=, else the browser's language), the hotel's own instructions, a card
to show a taxi driver, a walking route from where the guest is, and the door's QR to print. No gate photo: the API
serves it only to a names:write key, and a guest page's key is in every guest's browser.
The hotel is a building code or a door link from a QR (#door=7QSVX), the door name it holds (#name=Сансар зочид
буудал), or, until it has either, a place name to search (#place=Kempinski). Hotels write instructions in mn and
en only: other languages show the English text and say so. Never a machine translation passed off as theirs.
Serve sdk/js over http (npm run build, then e.g. `npx serve .`) and open /examples/hotel.html#key=dk_...&place=...
The key needs door:read, geocode:read, route:read and tiles:read; a free key has them. #name also needs the
door_names feature on the key's org, else 403 feature_off shows under the map.
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>dock: getting to your hotel</title>
<link href="https://dock.mn/sdk/v1/dock.css" rel="stylesheet">
<script src="https://dock.mn/sdk/v1/dock.js"></script>
<style>
body { margin: 0 auto; max-width: 640px; padding: 16px; font: 16px system-ui, sans-serif; }
#map { height: 560px; border-radius: 8px; }
#driver { border: 2px solid #111; border-radius: 8px; padding: 12px; font-size: 22px; margin: 12px 0; }
#driver small { display: block; font-size: 13px; color: #555; }
.warn { color: #92400e; }
nav { float: right; }
figure { margin: 12px 0; } figcaption { font-size: 13px; color: #555; }
</style>
</head>
<body>
<nav><select id="lang" aria-label="Language"></select></nav>
<h2 id="name">...</h2>
<p id="how"></p>
<p id="note" class="warn"></p>
<div id="map"></div>
<p><button id="walk" type="button"></button> <span id="trip"></span></p>
<div id="driver"></div>
<figure id="qr" hidden><img alt=""><figcaption></figcaption></figure>
<script type="module">
const { maplibregl } = dock;
const { Client, DockError, createMap, doorQrSvg, label, parseDoorLink } = dock;
const params = new URLSearchParams(location.hash.slice(1));
const client = new Client({ key: 'dk_...' });
const $ = (id) => document.getElementById(id);
// The page's own words. The hotel's instructions come from the door, in mn or en only.
const T = {
en: { name: 'English', walk: 'Walk here from where I am', tap: 'Or tap the map where you are.', min: (n) => `${n} min walk`,
driver: 'Show this to the taxi driver', none: 'No such place.', unofficial: 'This entrance is not matched to an official address yet. Follow the pin.',
entrance: (l) => `Entrance ${l}.`, noroute: 'No walking route found.', noloc: 'Location not available. Tap the map where you are.',
qr: 'Scan to open this entrance on a map.' },
mn: { name: 'Монгол', walk: 'Эндээс алхах зам', tap: 'Эсвэл газрын зураг дээр байгаа газраа дарна уу.', min: (n) => `${n} мин алхана`,
driver: 'Жолоочид үзүүлнэ үү', none: 'Ийм газар олдсонгүй.', unofficial: 'Энэ хаалга албан ёсны хаягтай тулгагдаагүй. Пинээр очно уу.',
entrance: (l) => `${l}-р хаалга.`, noroute: 'Алхах зам олдсонгүй.', noloc: 'Байршил олдсонгүй. Газрын зураг дээр дарна уу.',
qr: 'Уншуулбал энэ хаалга газрын зураг дээр нээгдэнэ.' },
ja: { name: '日本語', walk: '現在地から歩くルート', tap: '地図上の現在地をタップすることもできます。', min: (n) => `徒歩${n}分`,
driver: 'タクシーの運転手に見せてください', none: '該当する場所が見つかりません。', unofficial: 'この入口はまだ公式住所と照合されていません。ピンの位置に向かってください。',
entrance: (l) => `入口 ${l}。`, noroute: '徒歩ルートが見つかりません。', noloc: '現在地を取得できません。地図上の現在地をタップしてください。',
qr: 'スキャンすると地図でこの入口が開きます。', english: '(ホテルが英語で記入)' },
ko: { name: '한국어', walk: '현재 위치에서 걸어가는 길', tap: '또는 지도에서 현재 위치를 누르세요.', min: (n) => `도보 ${n}분`,
driver: '택시 기사에게 보여 주세요', none: '해당 장소를 찾을 수 없습니다.', unofficial: '이 입구는 아직 공식 주소와 대조되지 않았습니다. 핀을 따라가세요.',
entrance: (l) => `${l}번 입구.`, noroute: '도보 경로를 찾을 수 없습니다.', noloc: '위치를 확인할 수 없습니다. 지도에서 현재 위치를 누르세요.',
qr: '스캔하면 지도에서 이 입구가 열립니다.', english: '(호텔이 영어로 작성)' },
zh: { name: '中文', walk: '从我所在处步行前往', tap: '或在地图上点击您所在的位置。', min: (n) => `步行 ${n} 分钟`,
driver: '请给出租车司机看', none: '未找到该地点。', unofficial: '此入口尚未与官方地址核对。请按图钉前往。',
entrance: (l) => `${l}号入口。`, noroute: '未找到步行路线。', noloc: '无法获取位置。请在地图上点击您所在的位置。',
qr: '扫码在地图上打开此入口。', english: '(酒店以英文填写)' },
ru: { name: 'Русский', walk: 'Пешком отсюда', tap: 'Или нажмите на карте, где вы находитесь.', min: (n) => `${n} мин пешком`,
driver: 'Покажите это водителю такси', none: 'Место не найдено.', unofficial: 'Этот вход ещё не сверен с официальным адресом. Идите к метке.',
entrance: (l) => `Вход ${l}.`, noroute: 'Пешеходный маршрут не найден.', noloc: 'Местоположение недоступно. Нажмите на карте, где вы находитесь.',
qr: 'Отсканируйте, чтобы открыть этот вход на карте.', english: '(отель написал это по-английски)' },
};
const pick = (l) => (l && T[l.slice(0, 2).toLowerCase()] ? l.slice(0, 2).toLowerCase() : null);
let lang = pick(params.get('lang')) ?? pick(navigator.language) ?? 'en';
let hotel = null, from = null, map;
const fail = (e) => { $('trip').textContent = e instanceof DockError ? `${e.code}: ${e.message}` : String(e); };
// A door when the hotel has a code (typed, or the link its QR holds) or a door name; else the best geocode hit.
async function load() {
if (params.get('door')) return client.door(parseDoorLink(params.get('door'))?.code ?? params.get('door'));
if (params.get('name')) return (await client.doorByName(params.get('name'))).door;
const { results } = await client.geocode(params.get('place') ?? '', { limit: 1 });
return results[0] ?? null;
}
function render() {
const t = T[lang];
document.documentElement.lang = lang;
$('walk').textContent = t.walk;
if (!hotel) { $('name').textContent = t.none; return; }
// Names are mn or en; every other language reads the English one.
$('name').textContent = label(hotel, lang === 'mn' ? 'mn' : 'en');
const letter = hotel.entrance?.letter;
const own = lang === 'mn' || lang === 'en' ? hotel[`instructions_${lang}`] : null;
const said = own ?? (hotel.instructions_en && lang !== 'mn' ? `${hotel.instructions_en} ${t.english ?? ''}`.trim() : null);
$('how').textContent = [letter && t.entrance(letter), said, t.tap].filter(Boolean).join(' ');
$('note').textContent = hotel.unofficial ? t.unofficial : '';
// Always Mongolian for the driver, whatever the guest reads. The code lets a driver's app open the door.
const card = [label(hotel, 'mn'), letter && T.mn.entrance(letter)].filter(Boolean).join(' ');
$('driver').replaceChildren(Object.assign(document.createElement('small'), { textContent: t.driver }), card);
if (hotel.code) $('driver').append(Object.assign(document.createElement('small'), { textContent: `dock: ${hotel.code}` }));
// The door's QR holds its link: a camera opens the entrance on map.dock.mn. Only a door with a code has one.
const svg = hotel.code && doorQrSvg(hotel.code);
$('qr').hidden = !svg;
if (svg) {
$('qr').querySelector('img').src = `data:image/svg+xml,${encodeURIComponent(svg)}`;
$('qr').querySelector('figcaption').textContent = t.qr;
}
}
async function walk(point) {
from = point;
new maplibregl.Marker({ color: '#16a34a' }).setLngLat(point).addTo(map);
try {
const r = await client.route(point, hotel.code ?? hotel.point, { mode: 'walking' });
const data = { type: 'Feature', properties: {}, geometry: r.geometry };
if (map.getSource('walk')) map.getSource('walk').setData(data);
else {
map.addSource('walk', { type: 'geojson', data });
map.addLayer({ id: 'walk', type: 'line', source: 'walk', paint: { 'line-color': '#16a34a', 'line-width': 4, 'line-dasharray': [2, 1] } });
}
map.fitBounds(new maplibregl.LngLatBounds(point, point).extend(hotel.point), { padding: 50, maxZoom: 17 });
$('trip').textContent = `${Math.round(r.distance_m + r.to.gap_m)} m, ${T[lang].min(Math.max(1, Math.round(r.duration_s / 60)))}`;
} catch (e) {
if (e instanceof DockError && e.code === 'no_route') { $('trip').textContent = T[lang].noroute; return; }
fail(e);
}
}
$('lang').append(...Object.entries(T).map(([k, t]) => new Option(t.name, k, false, k === lang)));
$('lang').addEventListener('change', () => { lang = $('lang').value; render(); if (from && hotel) walk(from); });
$('walk').addEventListener('click', () => navigator.geolocation.getCurrentPosition(
(p) => walk([p.coords.longitude, p.coords.latitude]), () => { $('trip').textContent = T[lang].noloc; }));
try {
hotel = await load();
} catch (e) {
if (!(e instanceof DockError && e.code === 'not_found')) fail(e);
}
render();
if (hotel) {
map = new dock.Map({ client, ui: 'sheet', container: 'map', center: hotel.point, zoom: 17 });
new maplibregl.Marker({ color: '#dc2626' }).setLngLat(hotel.point).addTo(map);
map.on('click', (ev) => walk([ev.lngLat.lng, ev.lngLat.lat]));
}
</script>
</body>
</html>