Skip to content

Guest page ​

A hotel entrance in Mongolian, English, Japanese, Korean, Chinese or Russian. A card for the taxi driver, the walk, a QR when the door has a code.

↗

Code ​

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>