Skip to content

Same-day delivery ​

One driver, ten orders, seven districts. The driving order and an arrival time at each stop. No traffic yet, and the page says so.

↗

Code ​

html
<!doctype html>
<!--
  dock sample app: same-day delivery, one driver. Paste up to 11 orders, one per line, as customers wrote them: a door
  code ("7QSVX 17") or an address. dock puts them in the driving order with the least time on the road, from the depot
  and back, and gives each stop its arrival time from the start time and the minutes at each stop. Each leg is timed at
  its own hour: drivers' measured speeds of that hour band where dock has them, else OSM road speeds with no traffic.
  The page says which. 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/sameday.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: 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: 190px; }
    .row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
    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; }
    small { color: #666; }
    .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>
      <div class="row">
        <label>Гарах цаг<input name="start" type="time" value="10:00"></label>
        <label>Цэг бүрт, мин<input name="stay" type="number" min="0" max="60" value="5"></label>
      </div>
      <label>Захиалгууд, мөр бүрт нэг (11 хүртэл)<textarea name="orders">Улсын их дэлгүүр
Сансар Төв
Zaisan Star
Нарантуул зах
Толгойт
Сэлбэ сургууль
Хүннү молл
Цайз зах
Налайх татварын хэлтэс
Гандан</textarea></label>
      <button>Дараалал гаргах</button>
    </form>
    <p id="sum"></p>
    <p id="speeds"></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, OPTIMIZED_TRIPS_MAX, 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());
    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() };
      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), 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) } : null;
    }

    // Times are Ulaanbaatar's (UTC+8, no daylight saving), whatever the browser's zone.
    const UB = 8 * 3600_000;
    const hhmm = (ms) => new Date(ms + UB).toISOString().slice(11, 16);
    const iso = (ms) => new Date(ms + UB).toISOString().slice(0, 19) + '+08:00';
    function startMs(hm) {
      const day = new Date(Date.now() + UB).toISOString().slice(0, 10);
      return Date.parse(`${day}T${hm}:00+08:00`);
    }

    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 } });
    }

    const cell = (tr, text) => { const td = tr.insertCell(); td.textContent = text; return td; };

    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 stay = Number(form.get('stay')) * 60_000;
      const body = document.getElementById('stops');
      const sum = document.getElementById('sum');
      const speeds = document.getElementById('speeds');
      body.replaceChildren();
      speeds.textContent = '';
      markers.forEach((m) => m.remove());
      markers = [];
      if (lines.length > OPTIMIZED_TRIPS_MAX - 1) { sum.textContent = `${lines.length} захиалга: нэг жолоочид ${OPTIMIZED_TRIPS_MAX - 1} хүртэл`; return; }
      sum.textContent = 'Тооцоолж байна...';
      try {
        const depot = await resolve(String(form.get('depot')));
        if (!depot) { sum.textContent = 'Агуулах олдсонгүй'; return; }
        const found = [], missed = [];
        for (const text of lines) {
          const stop = await resolve(text);
          if (!stop) { missed.push(text); continue; }
          const { district_mn } = await client.reverse(stop.point, { limit: 1 });
          found.push({ ...stop, line: text, district: district_mn });
        }
        if (found.length === 0) { sum.textContent = 'Нэг ч захиалга олдсонгүй'; return; }
        // The order with the least time on the road, from the depot and back to it.
        const opt = await client.optimizedTrips([depot.end, ...found.map((s) => s.end)], { mode: 'driving' });
        const order = found.map((s, i) => ({ ...s, at: opt.waypoints[i + 1].waypoint_index })).sort((a, b) => a.at - b.at);
        // Each leg timed at its own departure: the hour band's measured speeds where dock has them, else free-flow.
        let clock = startMs(String(form.get('start'))), from = depot.end, km = 0, measuredM = 0, band = null;
        const legs = [...order, { end: depot.end, back: true }];
        for (const [i, stop] of legs.entries()) {
          const r = await client.route(from, stop.end, { mode: 'driving', departAt: iso(clock) });
          clock += (r.measured ? r.measured.duration_s : r.duration_s) * 1000;
          measuredM += r.measured?.roads_m ?? 0;
          band = r.measured?.band ?? band;
          km += r.distance_m / 1000;
          const tr = body.insertRow();
          cell(tr, stop.back ? '' : String(i + 1));
          const where = cell(tr, stop.back ? 'Агуулахад буцна' : stop.text);
          if (!stop.back) {
            const sub = document.createElement('small');
            sub.textContent = ` ${stop.district ?? 'дүүрэг тодорхойгүй'}` + (r.to.gap_m > 30 ? `, ${Math.round(r.to.gap_m)} м явган` : '');
            where.append(sub);
          }
          cell(tr, hhmm(clock));
          cell(tr, (r.distance_m / 1000).toFixed(1));
          if (!stop.back) {
            clock += stay;
            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));
          }
          from = stop.end;
        }
        for (const text of missed) {
          const tr = body.insertRow();
          tr.className = 'miss';
          cell(tr, '');
          cell(tr, `${text}: олдсонгүй, хэрэглэгчээс асуу`);
          cell(tr, ''); cell(tr, '');
        }
        markers.push(new maplibregl.Marker({ color: '#111' }).setLngLat(depot.point).addTo(map));
        drawLine('sameday-route', opt.trips[0].geometry.coordinates);
        const bounds = new maplibregl.LngLatBounds(depot.point, depot.point);
        order.forEach((s) => bounds.extend(s.point));
        map.fitBounds(bounds, { padding: 60, maxZoom: 15 });
        const districts = new Set(order.map((s) => s.district).filter(Boolean)).size;
        sum.textContent = `${order.length} цэг, ${districts} дүүрэг, ${km.toFixed(1)} км, буцаж ирэх ${hhmm(clock)}, олдоогүй ${missed.length}.`;
        speeds.textContent = measuredM > 0
          ? `Хурд: жолооч нарын хэмжсэн хурд ${(measuredM / 1000).toFixed(1)} км замд (${band ?? 'бүх цаг'}), бусад нь OSM-ийн хурдаар.`
          : 'Хурд: OSM-ийн замын хурдаар, түгжрэл тооцоогүй. Жолооч нарын хэмжсэн хурд гарвал тухайн цагийнхыг авна.';
      } catch (e) {
        sum.textContent = e instanceof DockError ? `${e.code}: ${e.message}` : String(e);
      }
    });
  </script>
</body>
</html>