# tilemap.js — Google-tile slippy map + suggest/directions client

Bộ 3 file trong `captured/` cho phép nhúng bản đồ tile Google (ẩn danh, không API key)
kèm autocomplete + chỉ đường vào **bất kỳ trang HTML nào**:

| File | Vai trò |
|---|---|
| `mapsApi.js` | Backend (port 4000) — proxy các endpoint ẩn danh `/suggest`, `/directions`, `/vt/:z/:x/:y` + serve static |
| `tilemap.js` | **Toàn bộ logic frontend** — render tilemap (raster **hoặc vector**), marker/polyline, API client, autocomplete. Zero dependency. |
| `vectorTile.js` | **Decoder vector tile** — XOR 0x9B + protobuf + geometry → roads/labels lat/lng. Zero dependency. |
| `mvt.js` | **Encoder Mapbox Vector Tile** (spec 2.1, zero-dep) — để phục vụ MapLibre. |
| `tilemap-demo.html` | Trang demo canvas riêng (tìm địa điểm, chỉ đường, POI click, đổi layer). |
| `maplibre-demo.html` | Trang demo **MapLibre GL** dùng MVT + style.json + marker icon + OSM buildings. |

## Hai chế độ render

| Chế độ | Nguồn | Nội dung |
|---|---|---|
| **`vector`** (mặc định demo) | `/vt/:z/:x/:y` → XOR protobuf **giải mã client-side** | mạng lưới đường + nhãn phố vẽ trên `<canvas>` — nét vector sắc, route nằm khớp trên đường |
| `raster` | `mt{0..3}.google.com/vt` PNG | ảnh bản đồ/giao thông/vệ tinh Google |

Cả hai đều **ẩn danh, không API key**. Route (polyline) + marker luôn vẽ trên lớp SVG phía trên nên khớp pixel với nền bên dưới ở cả hai chế độ.

## Chạy nhanh

```bash
node captured/mapsApi.js        # PORT=xxxx để đổi port
# → mở http://localhost:4000/demo
# → docs này:  http://localhost:4000/docs
```

Demo nhận sẵn URL params (như link chia sẻ):
- `/demo?from=197+tran+phu&to=cho+ben+thanh[&mode=driving]` — tự tìm & vẽ route
- `/demo?poi=0x…:0x…` — căn giữa map + mở popup detail 1 POI
- `/demo?center=lat,lng,zoom` — căn map tới vị trí
- `/demo?buildings=1&incidents=1` — bật sẵn overlay toà nhà / điểm tạm đóng
- `/demo?roadcolor=type` (hoặc `style`) — tô đường theo class ngay khi tải

## Overlay trên canvas demo (toà nhà · điểm tạm đóng)

Panel góc phải có 2 toggle:

**🏠 Toà nhà** (zoom ≥ 16) — tile Google ẩn danh KHÔNG có building polygon (xem mục
"Nhà" ở trên), nên nhà lấy **vector từ OpenStreetMap** qua Overpass API (keyless).
Server `GET /buildings?minLat=&minLng=&maxLat=&maxLng=` → `{rings:[[[lat,lng],…],…]}`,
canvas vẽ fill bằng `map.setPolygons()`. Overpass free hay bị rate-limit (HTTP 429) khi
gọi dồn — demo tự báo lỗi và thử lại; cache 5 phút.

**🚧 Điểm tạm đóng** — decode `field[17]` incident của vector tile → build FID từ 2
fixed64 (`id4`,`id1`) → geocode lat/lng qua **`/maps/preview/place`** (ẩn danh, không key).
Server `GET /incidents?bbox…` → `{incidents:[{kind,lat,lng,name,categories,text}]}`.

> **Thành thật về "traffic" & "cấm đường":**
> - **Traffic flow** (tô màu xanh/đỏ đường): geometry nằm trong blob mã hoá riêng ở
>   `field[8].[1][8]` — Google cố tình obfuscate, chưa decode ra vector được.
> - **Cấm đường thật**: `field[17]` cat-20 dùng CHUNG icon "closed" cho **mọi** thứ tạm
>   đóng (spa, quán ăn, nhà văn hoá…), **không** tách được cấm-đường-thật với venue tạm
>   đóng chỉ từ tile. Cấm đường thật là entity riêng, không nằm đầy đủ trong protobuf này.
>   → Overlay này chỉ hiện **điểm tạm đóng** (đa số là địa điểm); cái có category đường mới
>   tô đỏ ⛔, còn lại vàng ⚠.
> - Muốn **traffic flow + cấm đường thật** thì dùng layer **Giao thông** (raster
>   `lyrs=m,traffic`) — Google nướng sẵn màu flow + icon cấm đường vào PNG.

---

# MapLibre GL — vector tile chuẩn (MVT), không API key

Ngoài renderer canvas tự viết (`tilemap.js`), server còn phát **Mapbox Vector Tile chuẩn**
để nạp thẳng vào **MapLibre GL** (thư viện mã nguồn mở, **không cần key/token** như Mapbox GL):

| Endpoint | Nội dung |
|---|---|
| `GET /mvt/{z}/{x}/{y}.pbf` | MVT chuẩn (spec 2.1) — 3 layer: `road` (line), `road_label` (line+name), `poi` (point: name, fid, kind) |
| `GET /tiles.json` | TileJSON trỏ tới `/mvt` (minzoom 11, maxzoom 18) |
| `GET /style.json` | **Style MapLibre dựng sẵn** (nền, road casing+fill, nhãn đường, POI) — chỉ việc trỏ MapLibre vào đây |
| `GET /maplibre` | Trang demo MapLibre đầy đủ |

Server fetch tile Google ẩn danh → `vectorTile.js` decode → `mvt.js` **re-encode sang MVT** →
MapLibre vẽ. Toạ độ đã verify round-trip đúng (POI decode ngược ra đúng lat/lng HCMC).

### Nạp vào MapLibre (chỉ 3 dòng)

```html
<link href="https://unpkg.com/maplibre-gl@4/dist/maplibre-gl.css" rel="stylesheet">
<script src="https://unpkg.com/maplibre-gl@4/dist/maplibre-gl.js"></script>
<script>
  const map = new maplibregl.Map({
    container: 'map',
    style: 'http://localhost:4000/style.json',   // ← style + nguồn MVT dựng sẵn
    center: [106.6661, 10.7605], zoom: 15,
  });
</script>
```

- **Glyphs** (font cho nhãn) lấy từ CDN công cộng keyless `fonts.openmaptiles.org` — đổi/self-host được.
- **Marker icon POI**: `/maplibre` tự **generate pin bằng canvas** (`addImage`) rồi gán `icon-image`
  cho layer `poi` — không cần sprite, không key. Style.json mặc định dùng chấm tròn để portable.
- **Detail POI tách riêng**: click feature layer `poi` → lấy `fid` từ thuộc tính tile →
  gọi `/place` (keyless) → popup. Detail **không nhúng vào tile**, chỉ fetch khi click.

### Nhà (buildings) — KHÔNG có trong tile ẩn danh

Đã kiểm tra kỹ: tile Google ẩn danh (decode được) **không chứa building polygon**.
- `field[7]` = roads (line), `field[10]` = labels, `field[8]` = **flow** (414 line mở,
  geometry blob mã hoá khác — không phải nhà), **không có** container polygon/`field[9]`.
- Bản base tile "sạch" (có nhà) trả về container **"DRAT"** — định dạng nén riêng của
  Google, chưa giải được (thử gzip/inflate/brotli mọi offset đều fail).
- `maps/api/elevation` & building qua API chính thức đều **bắt buộc API key**.

→ Muốn nhà thì phải: (a) dùng tile **có API key** (container polygon đầy đủ), hoặc
(b) nguồn ngoài như OpenStreetMap/Overpass. Bản demo này **không** kèm nhà để giữ đúng
nguyên tắc "chỉ từ tile Google, không key".

`tilemap-demo.html` cũng mở được trực tiếp bằng `file://` — nó tự trỏ API về
`http://localhost:4000` khi không được serve từ port 4000.

## Tích hợp tối thiểu vào trang HTML khác

```html
<div id="map" style="height:100vh"></div>
<script src="http://localhost:4000/vectorTile.js"></script>  <!-- chỉ cần khi render:'vector' -->
<script src="http://localhost:4000/tilemap.js"></script>
<script>
  const { TileMap, MapsApiClient, attachSearchBox } = window.GmapTilemap;

  const map = new TileMap(document.getElementById('map'), {
    center: { lat: 10.7605, lng: 106.6661 },   // HCMC
    zoom: 15,
    render: 'vector',                          // nền vector (bỏ dòng này = raster)
    apiBase: 'http://localhost:4000',          // để fetch /vt (vector mode)
  });

  const api = new MapsApiClient('http://localhost:4000');

  // Vẽ 1 route lên map (khớp trên đường vector):
  const res = await api.directions('197 tran phu', 'cho ben thanh');
  const pts = res.routes[0].path.map(p => [p.lat, p.lng]);
  map.addPolyline(pts);
  map.addMarker(res.origin.lat, res.origin.lng, { color: '#188038', label: 'A' });
  map.addMarker(res.destination.lat, res.destination.lng, { label: 'B' });
  map.fitBounds([[res.origin.lat, res.origin.lng], [res.destination.lat, res.destination.lng]]);
</script>
```

## Nguồn tile

```
https://mt{0..3}.google.com/vt?lyrs=<lyrs>&hl=<lang>&x=<x>&y=<y>&z=<z>
```

Không cần auth/API key (cùng endpoint pipeline traffic của project). Giá trị `lyrs`:

| `lyrs` | Nội dung |
|---|---|
| `m` | roadmap (mặc định) |
| `m,traffic` | roadmap + lớp giao thông live |
| `s` | ảnh vệ tinh |
| `y` | hybrid (vệ tinh + nhãn) |
| `t` | địa hình |

Subdomain xoay vòng `mt0…mt3` theo `(x+y) % 4`. Đổi layer lúc runtime: `map.setLyrs('m,traffic')`.

## Nguồn vector tile (ẩn danh, không API key)

Endpoint protobuf mà chính Google Maps web dùng — **không cần API key, không cookie**:

```
https://www.google.com/maps/vt?pb=!1m4!1m3!1iZ!2iX!3iY
   !2m3!1e0!2sm!3i<version>
   !3m7!2sen!5e1105
   !12m4!1e68!2m2!1sset!2sRoadmap
   !4e1                                  ← output = XOR'd protobuf (không phải PNG)
   !5m4!1e4!8m2!1e0!1e1                  ← cờ flow  ┐ chính các cờ này ép response
   !6m14!1e12!2i2!19m1!1e0!20m1!1e0      │          │ về protobuf thay vì container
   !26m2!1b1!4b1!39b1!44e1!50e0          ← cờ sự cố ┘ nhị phân "DRAT" khó giải
   !67m1!1e1
```

> **Gotcha:** pb tối giản (`!4e1!5m1!1e0`) trả về container **"DRAT"** (nén/mã hoá riêng, chưa giải được). Phải giữ đủ bộ cờ `!5m4…!6m14…!26m2…!67m1!1e1` mới nhận được XOR-protobuf giải mã được.

`mapsApi.js` proxy endpoint này ở `/vt/:z/:x/:y` (browser không fetch chéo domain google.com được) và trả **nguyên bytes còn XOR** — bước XOR + protobuf + geometry chạy client-side trong `vectorTile.js`.

### Pipeline giải mã (trong `vectorTile.js`, port từ logic project)

1. `bytes ⊕ 0x9B` → protobuf chuẩn.
2. `parseProtobuf` → cây `{fieldNum → entry}` (schema-less, có packed varints cho geometry).
3. **extent** = `EXTENT_BY_LEVEL[ tree[2][2] ]` (level enum → hệ số chia toạ độ; ví dụ level 1 → 16). ⚠️ Đây là điểm dễ sai nhất — dùng nhầm 4096/65536 thì mọi đường co về 1 điểm.
4. **roads** = `tree[7][1][*]`: `feature.field[1]` = packed **zigzag-delta** `[dx0,dy0,dx1,dy1,…]` → cộng dồn `(rawX,rawY)` → `/extent` → toạ độ pixel trong tile (0..256) → Web-Mercator → lat/lng. `field[2]` = mốc đếm điểm tích luỹ → tách thành nhiều sub-path.
5. **labels** = `tree[10][1][*]` — text nằm ở **nhiều vị trí** tuỳ loại nhãn (điểm dễ sai thứ 2):
   - POI có tiêu đề → `[2]`→(lặp)`[1]`→`[1]` (tên nhiều dòng)
   - POI dạng địa chỉ → `[53863091][5]`
   - **tên đường** → `[1]`→`[1]` (field[1] là message)
   - `cleanLabel()` cắt bỏ byte-tag rác dính đầu chuỗi (vd `"\n\fNhật Tảo"`) và loại bỏ chuỗi FID/place_id.
   - **vị trí**: POI = điểm đơn `[3][1][1]` (zigzag `[x,y]`); **đường** = polyline `[4][1]` (zigzag-delta) → vẽ tên xoay dọc đường. `renderType [14]==160` = nhãn đường.
   - **FID** POI ở `[53863091][20]` (dùng để click → lấy detail).

Vector tile ẩn danh này chứa **roads (field 7) + labels (field 10: POI + tên đường)** — không có water/polygon như tile base có API key. Vừa đủ để dựng nền mạng-lưới-đường sạch + tên đường + POI click được.

## Click POI → detail (giống bản gốc)

Ở chế độ vector, mỗi POI vẽ 1 chấm đỏ + tên, có **hit-box click**. Click → map phát event `poiclick` với `{text, lat, lng, fid, screen}`. Dùng FID gọi:

```
GET /place?fid=0x…:0x…
```

`mapsApi.js` gọi **ApplicationService.GetEntityDetails** ẩn danh (không API key — path project đã dùng) và parse **toàn bộ** field API trả về, kèm `raw` (mảng entity gốc — không giấu gì):

```js
{ ok, fid, name, category,
  rating, reviews, reviewsUrl,
  phone, telLink, website, websiteLabel,
  address, shortAddress, addressParts, lat, lng,
  cid, placeId, plusCode, plusCodeLocal,
  streetViewThumb,              // ảnh street-view thumbnail (hiện trên popup)
  openStatus,                   // "Đang mở cửa · Đóng cửa vào 22:30"
  hours: [{ day, hours }],      // giờ mở cửa từng ngày
  updateNote, mapsUrl,
  raw }                         // ← mảng entity gốc, đầy đủ 100%
```

Popup demo hiển thị **tất cả** field có giá trị: thumbnail · tên · category · ★rating (link review) · trạng thái mở cửa · địa chỉ · điện thoại (bấm gọi) · website · plus-code · toạ độ · place_id/fid/cid · giờ mở cửa (bung ra) · **"Xem toàn bộ dữ liệu API (raw)"** (JSON đầy đủ) · nút "Chỉ đường tới đây" / "Mở Google Maps". Popup tự lật lên/xuống theo chỗ trống.

Field map bên trong `en = response[1]` (ghi trong `placeDetail` của mapsApi.js): `[1]` tên · `[3]` rating · `[4]` review · `[7]` phone · `[11][0]` website · `[12]` category · `[16][0][2][0]` streetview thumb · `[27]` place_id · `[38][1][0]` plus-code · `[39]` giờ mở cửa.

---

# API reference (`window.GmapTilemap`)

## `new TileMap(container, opts)`

`container` là block element **đã có chiều cao**. Options:

| Option | Mặc định | Ý nghĩa |
|---|---|---|
| `center` | `{lat:10.7605107, lng:106.6661053}` | tâm ban đầu |
| `zoom` | `13` | zoom ban đầu (số nguyên) |
| `render` | `'raster'` | `'raster'` (PNG Google) hoặc `'vector'` (decode canvas) |
| `apiBase` | `''` | base URL của mapsApi.js để fetch `/vt` (cần cho vector) |
| `lyrs` | `'m'` | loại tile raster (bảng trên) |
| `lang` | `'vi'` | ngôn ngữ nhãn trên tile |
| `minZoom` / `maxZoom` | `3` / `20` | giới hạn zoom |

> Vector mode cần thẻ `<script src="vectorTile.js">` được load trước `tilemap.js`. Nhãn phố chỉ vẽ từ zoom ≥ 14.

Tương tác có sẵn: kéo chuột để pan, lăn chuột / double-click / nút `+ −` để zoom
(zoom neo theo vị trí con trỏ). Zoom là số nguyên, không có pinch cảm ứng.

### Methods

| Method | Mô tả |
|---|---|
| `setView(lat, lng, zoom?)` | nhảy tới vị trí |
| `getCenter()` / `getZoom()` | trạng thái hiện tại |
| `setZoom(z)` | zoom quanh tâm |
| `setRender(mode)` / `getRender()` | đổi `'raster'` ↔ `'vector'` lúc runtime |
| `setRoadColorBy(mode)` / `getRoadColorBy()` | tô đường theo class (vector): `null` (mặc định trắng) · `'type'` (field[11] level) · `'style'` (field[5]) — cùng class cùng màu |
| `setLyrs(lyrs)` / `getLyrs()` | đổi loại tile raster ngay lập tức |
| `fitBounds([[lat,lng],[lat,lng]], {padding=48, maxZoom=17})` | khớp viewport vào bbox |
| `addMarker(lat, lng, opts)` → handle | ghim pin. `opts`: `color` (mặc định đỏ), `label` (chữ trong pin, vd `'A'`), `title`, `onClick(marker)` |
| `addPolyline(points, opts)` → handle | vẽ đường. `points` = `[[lat,lng],…]`. `opts`: `color` (mặc định `#1a73e8`), `weight=5`, `opacity=1`, `onClick(line)` |
| `clearOverlays()` | xoá mọi marker + polyline |
| `on(event, fn)` | đăng ký sự kiện |
| `destroy()` | gỡ map khỏi DOM |

**Marker handle:** `{ lat, lng, remove(), setLatLng(lat, lng) }`
**Polyline handle:** `{ points, remove(), setStyle({color,weight,opacity}), bringToFront(), bounds() }`
(`bounds()` trả bbox — đưa thẳng vào `fitBounds`.)

### Events

| Event | Payload | Khi nào |
|---|---|---|
| `click` | `{lat, lng}` | click (không phải kéo) trên nền map |
| `poiclick` | `{text, lat, lng, fid, screen}` | click trúng 1 POI (chỉ ở chế độ vector) |
| `moveend` | `{lat, lng}` | kết thúc pan / setView / fitBounds |
| `zoomend` | `zoom` | sau khi đổi zoom |

## `new MapsApiClient(baseUrl)`

Client cho `mapsApi.js`. `baseUrl` = `''` nếu trang được serve cùng origin,
ngược lại `'http://localhost:4000'`.

### `await api.suggest(q, {lat, lng, zoom, limit, lang})`

Gọi `GET /suggest`. Bias kết quả quanh `lat/lng`. Trả:

```js
{
  ok: true, query, biasCenter, count,
  suggestions: [{
    type: 'place' | 'query',
    description, mainText, secondaryText,
    // chỉ có với type='place':
    location: { lat, lng } | null,
    fid,        // '0x…:0x…'
    placeId,    // 'ChIJ…'
    googleId,   // '/g/…'
    countryCode,
  }],
}
```

### `await api.directions(from, to, {mode, lang, compact})`

Gọi `GET /directions`. `from`/`to` nhận **text tự do hoặc `"lat,lng"`** —
text được geocode qua chính endpoint suggest. Trả:

```js
{
  ok: true,
  origin:      { input, resolvedBy: 'suggest'|'coords', name, lat, lng, fid?, placeId? },
  destination: { … },
  routeCount,
  routes: [{
    index, summary,
    distance: { meters, text },
    duration: { seconds, text },
    polyline,          // encoded polyline5
    pointCount,
    path,              // [{lat,lng},…]  (bỏ khi compact:true)
    steps: [{ instruction, distance, duration, location }],  // (bỏ khi compact:true)
  }],
}
```

Cả hai method **throw `Error`** với message từ server khi thất bại — nhớ try/catch.

## `attachSearchBox(inputEl, opts)`

Gắn dropdown autocomplete (gọi `/suggest`, debounce 250 ms, điều hướng ↑↓/Enter/Esc)
vào một `<input>` sẵn có:

| Option | Mặc định | Ý nghĩa |
|---|---|---|
| `api` | **bắt buộc** | `MapsApiClient` |
| `map` | — | dùng làm bias center + hành vi mặc định khi chọn |
| `limit` / `lang` | `5` / `'en'` | tham số suggest |
| `minChars` / `debounceMs` | `2` / `250` | ngưỡng gõ |
| `placeMarker` | `true` | mặc định khi pick: `setView` + thả marker (cần `map`) |
| `onPick(suggestion)` | — | override hành vi khi chọn |

Trả `{ getPicked(), destroy() }` — `getPicked()` cho suggestion cuối được chọn
(dùng lấy toạ độ chính xác thay vì gửi lại text).

## Tiện ích

- `decodePolyline5(str)` → `[[lat,lng],…]` — giải mã `routes[i].polyline`.
- `project(lat, lng, zoom)` / `unproject(x, y, zoom)` — Web Mercator ↔ world pixel (world = `256·2^z` px).

# API reference (`window.GmapVectorTile`) — decoder vector tile

Dùng khi bạn muốn giải mã vector tile trực tiếp (ngoài TileMap).

### `decodeVectorTile(bytes, {z, x, y})`

`bytes` = `Uint8Array`/`ArrayBuffer` **nguyên bytes còn XOR** từ `/vt/:z/:x/:y`. Trả:

```js
{
  coords: { z, x, y },
  extent,        // hệ số chia toạ độ (đọc từ field[2][2])
  level,
  roads: [{
    paths: [ [[lat,lng],…], … ],   // các sub-path đã tách
    styleClass,                     // field[5] — nhóm style đường
    typeCode,                       // field[11] — mã loại/style
  }],
  labels: [{
    type: 'poi' | 'road',
    text, lat, lng,
    fid,             // chỉ POI — "0x…:0x…", dùng cho /place
    polyline,        // chỉ road — [[lat,lng],…] để vẽ tên xoay dọc đường
    styleCode, renderType,
  }],
}
```

### `xorTransform(bytes)` / `parseProtobuf(buf)` / `EXTENT_BY_LEVEL`

Các hàm cấp thấp nếu cần tự dựng pipeline (XOR 0x9B; parse protobuf schema-less; bảng level→extent).

---

# Ghi chú & hạn chế

- **Ẩn danh = luôn route ô tô.** Tham số `mode` chỉ có tác dụng khi request mang
  cookie phiên Google; server-side gọi ẩn danh nên xe máy/đi bộ/transit trả về
  giống driving (đã ghi chú trong UI demo).
- Endpoint suggest **không cần `psi`/`ech`** — psi cũ gây HTTP 500. Chỉ cần `pb` viewport.
- Zoom nguyên, không pinch-zoom cảm ứng, không rotate — đủ cho tooling nội bộ.
- Geometry route từ nguồn ẩn danh là junction-level (điểm rẽ + điểm nội suy),
  không dày như polyline chính thức — đủ để hiển thị, không dùng làm map-matching.
- **Vector tile chỉ có roads + labels** (không POI/water/polygon như tile có API key)
  → nền là mạng-lưới-đường sạch, không có mảng nước/công viên. Đúng mục đích hiển thị route.
- **Mật độ tên đường** bị giới hạn bởi dữ liệu Google: mỗi tile chỉ gán tên cho các
  đường chính (~13 nhãn/tile ở z15) — geometry đường thì đầy đủ (vài trăm sub-path).
  Zoom vào (z16-17) sẽ lộ thêm tên (kể cả hẻm). Render dedupe theo khoảng cách nên
  đường dài được lặp tên ở nhiều đoạn thay vì chỉ 1 lần.
- **Không có cao độ (elevation) đường.** Vector tile 2D thuần: road feature chỉ có
  `[1]` geometry (x,y), `[2]` ranges, `[5]` style, `[11]` type — **không có trục z**.
  Các endpoint elevation ẩn danh đều 404; `maps/api/elevation` bắt buộc API key.
  Muốn cao độ phải dùng Google Elevation API (có key) hoặc DEM ngoài (SRTM/Copernicus).
- **`VECTOR_TILE_VERSION`** trong `mapsApi.js` (`!3i<ver>`) có thể hết hạn → `/vt`
  trả HTTP 400/rỗng. Sniff giá trị mới: DevTools → Network → lọc `vt?pb` → `!3i<ver>!`.
- **extent** đọc từ `field[2][2]` per-tile (bảng `EXTENT_BY_LEVEL`) — đừng hardcode,
  sai extent thì toàn bộ đường co về góc tile.
- Tile © Google — chỉ dùng nghiên cứu nội bộ, không deploy công khai.
