Skip to content

Outlet map ​

A bank or pharmacy chain's branches in the demo district. Green where a person confirmed the door; grey otherwise, never shown as a door.

↗

Code ​

html
<!doctype html>
<!--
  dock sample app: outlet map. A public chain's branches (a bank, a pharmacy) in the demo district (PLAN.md 21:
  west 106.80, south 47.85, east 106.95, north 47.92), the stand-in for a distributor's outlets. Type the chain's name;
  every place of that name inside the district is listed and drawn. A branch is a door only when a person stood at it
  and confirmed it: then it has a code. Until then it is shown grey and "албан бус", never as a door. The outlets JSON
  is printed for your system. Branches come from the names dock holds, not from the chain's own list: say so to them.
  Serve sdk/js over http (npm run build, then e.g. `npx serve .`) and open /examples/outlet.html#key=dk_...
  The key needs geocode: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%; }
    input { width: 100%; box-sizing: border-box; padding: 6px; font: inherit; }
    table { width: 100%; border-collapse: collapse; margin-top: 10px; }
    td, th { padding: 4px; border-bottom: 1px solid #eee; text-align: left; vertical-align: top; }
    tbody tr { cursor: pointer; }
    tbody tr:hover { background: #eef; }
    .door { color: #15803d; font-weight: 600; }
    .guess { color: #6b7280; }
    pre { background: #f6f6f6; padding: 8px; overflow: auto; font-size: 12px; }
    @media (max-width: 700px) { body { grid-template-columns: 1fr; grid-template-rows: auto 60vh; } }
  </style>
</head>
<body>
  <aside>
    <form id="chain">
      <label>Сүлжээний нэр<input name="q" list="chains" value="Голомт банк"></label>
      <datalist id="chains"><option>Голомт банк</option><option>Хаан банк</option><option>Худалдаа хөгжлийн банк</option><option>Төрийн банк</option><option>Монос эмийн сан</option></datalist>
      <button>Харуулах</button>
    </form>
    <p id="sum"></p>
    <p id="src" class="guess"></p>
    <table><thead><tr><th>#</th><th>Салбар</th><th>Хаалга</th></tr></thead><tbody id="rows"></tbody></table>
    <p><button id="export" type="button" hidden>Салбаруудын JSON</button></p>
    <pre id="out" hidden></pre>
  </aside>
  <div id="map"></div>
  <script type="module">
    const { maplibregl } = dock;
    const { Client, DockError, createMap, label } = dock;

    // The demo district (PLAN.md 21), [west, south, east, north].
    const DISTRICT = [106.80, 47.85, 106.95, 47.92];
    const CENTER = [(DISTRICT[0] + DISTRICT[2]) / 2, (DISTRICT[1] + DISTRICT[3]) / 2];
    const params = new URLSearchParams(location.hash.slice(1));
    const client = new Client({ key: 'dk_...' });
    const map = createMap(client, { container: 'map', center: CENTER, zoom: 13 });
    map.addControl(new maplibregl.NavigationControl());
    const $ = (id) => document.getElementById(id);
    const form = $('chain');
    let markers = [], outlets = [];

    const inside = ([lon, lat]) => lon >= DISTRICT[0] && lon <= DISTRICT[2] && lat >= DISTRICT[1] && lat <= DISTRICT[3];
    const norm = (s) => (s ?? '').toLowerCase().replace(/\s+/g, ' ').trim();
    // A branch carries the chain's name at its start: "Голомт банк" keeps "Голомт банк Зайсан", drops "Голомт хотхон".
    const ofChain = (p, chain) => [label(p, 'mn'), label(p, 'en')].some((n) => norm(n).startsWith(chain));

    // Only a code means a person confirmed the door; everything else is a place of that name, not a door.
    async function show(name) {
      const chain = norm(name);
      if (!chain) return;
      $('sum').textContent = '...';
      const { results } = await client.geocode(name, { limit: 20, proximity: CENTER, types: ['door', 'business', 'building', 'landmark'] });
      outlets = results.filter((p) => inside(p.point) && ofChain(p, chain));
      markers.forEach((m) => m.remove());
      markers = outlets.map((p) => new maplibregl.Marker({ color: p.code ? '#15803d' : '#9ca3af' })
        .setLngLat(p.point).setPopup(new maplibregl.Popup().setText(label(p))).addTo(map));
      $('rows').replaceChildren(...outlets.map((p, i) => {
        const tr = document.createElement('tr');
        const door = document.createElement('td');
        door.className = p.code ? 'door' : 'guess';
        door.textContent = p.code ?? '';
        tr.append(Object.assign(document.createElement('td'), { textContent: String(i + 1) }),
          Object.assign(document.createElement('td'), { textContent: label(p) }), door);
        tr.onclick = () => { map.flyTo({ center: p.point, zoom: 17 }); markers[i].togglePopup(); };
        return tr;
      }));
      const doors = outlets.filter((p) => p.code).length;
      const official = outlets.filter((p) => !p.unofficial).length;
      $('sum').textContent = outlets.length
        ? `${name}: демо дүүрэгт ${outlets.length} салбар, баталгаажсан хаалга ${doors}, албан ёсны хаягтай ${official}.`
        : `${name}: демо дүүрэгт салбар олдсонгүй.`;
      $('src').textContent = outlets.length
        ? `Эх сурвалж: ${[...new Set(outlets.map((p) => p.source ?? 'тодорхойгүй'))].join(', ')}. Сүлжээний өөрийн жагсаалт биш; саарал нь хаалга биш, газрын нэр.`
        : '';
      $('export').hidden = !outlets.length;
      $('out').hidden = true;
      map.fitBounds([[DISTRICT[0], DISTRICT[1]], [DISTRICT[2], DISTRICT[3]]], { padding: 20 });
    }

    // What your system keeps per outlet: the code when the door is confirmed, the official address when matched.
    $('export').onclick = () => {
      $('out').hidden = false;
      $('out').textContent = JSON.stringify(outlets.map((p) => ({
        label_mn: label(p, 'mn'), label_en: label(p, 'en'), point: p.point,
        door_code: p.code ?? null, door_confirmed: Boolean(p.code),
        official_address: p.official_address, unofficial: p.unofficial, source: p.source,
      })), null, 2);
    };

    const fail = (e) => { $('sum').textContent = e instanceof DockError ? `${e.code}: ${e.message}` : String(e); };
    form.addEventListener('submit', (ev) => { ev.preventDefault(); show(form.q.value).catch(fail); });
    map.on('load', () => {
      map.addSource('district', { type: 'geojson', data: { type: 'Feature', properties: {}, geometry: { type: 'Polygon', coordinates: [[
        [DISTRICT[0], DISTRICT[1]], [DISTRICT[2], DISTRICT[1]], [DISTRICT[2], DISTRICT[3]], [DISTRICT[0], DISTRICT[3]], [DISTRICT[0], DISTRICT[1]]]] } } });
      map.addLayer({ id: 'district', type: 'line', source: 'district', paint: { 'line-color': '#1d4ed8', 'line-width': 2, 'line-dasharray': [2, 2] } });
      show(form.q.value).catch(fail);
    });
  </script>
</body>
</html>