npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kztek/sdk-pgs

v0.3.9

Published

KZTEK Parking Guidance System SDK — hiển thị bản đồ bãi xe, trạng thái ô đỗ real-time và chỉ đường xuyên tầng (MapLibre GL JS + ngraph.path)

Readme

@kztek/sdk-pgs — Parking Guidance System SDK

SDK hiển thị bản đồ bãi xe và chỉ đường, dùng chung cho mọi web app. Dựng trên MapLibre GL JS + ngraph.path (A*). Mỗi project chỉ cần npm install, khai báo URL API và token. SDK tự gọi API, vẽ bản đồ, cập nhật real-time và chỉ đường xuyên tầng.

SDK chỉ hiển thị và chỉ đường, không vẽ hay sửa bản đồ. Slot, node và cạnh của layout do trang quản trị lưu qua pgs-service (POST /slots, PUT /maps/{id}/layout); SDK chỉ đọc.

| Thành phần | Dùng cho | | -------------------------------- | ------------------------------------------------------------------------------------------- | | MapViewer / <pgs-map-viewer> | Kiosk, app tài xế: trạng thái ô đỗ real-time, tìm xe theo biển số / mã ô, chỉ đường đa tầng | | PgsRouter | Engine tìm đường, chạy được độc lập không cần bản đồ | | PgsApiClient | Đọc dữ liệu qua REST API (tự gắn token) | | SlotStream | Nhận sự kiện ô đỗ qua RabbitMQ Web-STOMP (@stomp/rx-stomp), tự kết nối lại |

Không phụ thuộc framework: dùng bằng Web Component hoặc class JS (Angular, React, Vue, HTML thuần).


Cài đặt

npm install @kztek/sdk-pgs maplibre-gl @stomp/rx-stomp rxjs

Thêm CSS của MapLibre (bắt buộc). Với Angular, khai báo trong angular.json:

"styles": ["node_modules/maplibre-gl/dist/maplibre-gl.css", "src/styles.scss"]

CSS của SDK tự inject, mọi class đều có tiền tố pgs-.


GeoMapViewer — bản đồ bãi trên toạ độ thật (GPS)

Type riêng, không thay đổi MapViewer. Mỗi tầng được neo vào kinh / vĩ độ thật → ảnh bản vẽ, ô đỗ, thiết bị, mạng đường đi nằm đúng chỗ trên nền bản đồ thế giới (kiểu Google Maps / Mapxus, có toà nhà 3D), vị trí GPS của điện thoại vẽ thẳng lên bản đồ (không quy đổi tương đối).

import { GeoMapViewer } from '@kztek/sdk-pgs';

const viewer = new GeoMapViewer({
  container: '#map', apiUrl, token,
  // Chỉ cần toạ độ TÂM ẢNH của từng map [kinh độ, vĩ độ]; mọi thứ trong map đặt theo map
  placement: { '<id map>': [105.80123, 21.00456] },
  basemap: 'light',       // mặc định: nền xám nhạt tối giản · 'streets' · 'satellite' · URL style MapLibre
  flat: true,              // mặc định: mặt bằng 2D nhìn thẳng; false = 3D (pitch, extrude)
  guideFromUrl: true,      // ?kiosk=&slot=&plate= từ QR
  realtime: { stomp },
});
await viewer.ready;
viewer.search('30A12345');
  • Đặt map lên toạ độ thật = toạ độ tâm ảnh của map + tỉ lệ metersPerPixel + hướng northOffset của map (đã có trên backend). Ô đỗ, Kiosk, thiết bị, đường đi thuộc map nên tự nằm đúng chỗ. (Nâng cao: { lngLat, pixel | deviceId } để dùng toạ độ đo tại một điểm khác tâm ảnh.) Chưa truyền placement thì đọc latitude / longitude (toạ độ tâm ảnh) của map — đề xuất backend thêm 2 field này để khỏi cấu hình ở client. Tầng chưa neo / chưa có tỉ lệ thì bỏ qua (cảnh báo console).
  • GPS: fix có sai số ≤ gps.maxAccuracy (25 m) mới dùng làm điểm xuất phát, hút vào mạng đường trong snapRadius (6 m), tính lại đường mỗi rerouteDistance (3 m); vòng sai số vẽ theo mét thật; chấm trượt mượt; chùm sáng la bàn hướng bắc thật; báo tới nơi trong arriveRadius (5 m). Chưa có GPS tốt thì xuất phát từ Kiosk (QR / origin).
  • Hiển thị: mặc định mặt bằng 2D phẳng trên nền xám nhạt (kiểu Mapxus), icon thiết bị như MapViewer (mốc = ghim màu + tên, showDevices), ghim đích, mũi tên chiều đi dọc đường chỉ dẫn. flat: false để xem 3D (pitch 55°, extrude: ô có xe thành khối, toà nhà). Click ô → chỉ đường. Realtime trạng thái ô như MapViewer.
  • Lọc GPS (Kalman): gộp các fix liên tiếp theo độ tin cậy → vị trí ổn định, vòng sai số nhỏ dần khi đứng yên / đi chậm; fix nhảy bất thường bị giảm trọng số; đã hút vào đường thì vòng sai số tối đa bằng snapRadius.
  • Giới hạn: GPS chỉ chính xác ngoài trời / bãi lộ thiên / tầng trệt gần cửa (3–10 m); tầng hầm sâu gần như không có GPS → dùng MapViewer (dẫn đường theo mốc).

MapPlacementEditor — công cụ "Đặt lên bản đồ"

Cho trang cấu hình map: nền bản đồ thế giới (vệ tinh / đường phố) kèm ảnh bản vẽ đúng tỉ lệ và hướng; quản trị kéo ảnh tới đúng vị trí bãi xe thật (tìm địa chỉ, chỉnh độ đậm ảnh, xoay ±1° / ±5°, tỉ lệ ±2% nếu cần) → getValue() trả { lngLat (tâm ảnh), northOffset, metersPerPixel } để lưu vào latitude / longitude / northOffset / metersPerPixel của map. Sự kiện change mỗi lần kéo / chỉnh xong.

const editor = new MapPlacementEditor({ container, imageUrl, width, height, metersPerPixel, northOffset, lngLat: [lng, lat] });
editor.on('change', (v) => form.patchValue(v));

Sử dụng

1. Web Component (khuyến nghị, không cần viết wrapper)

// main.ts: đăng ký một lần
import { defineElements } from "@kztek/sdk-pgs";
defineElements();
<pgs-map-viewer
  #viewer
  api-url="https://parking.example.com"
  [attr.token]="token"
  poll-interval="5000"
  origin="KIOSK_DEVICE_ID"
  style="height: 100vh"
  (pgs-slotclick)="onSlot($event.detail)"
>
</pgs-map-viewer>

Angular: thêm schemas: [CUSTOM_ELEMENTS_SCHEMA] vào component. Để gọi API của SDK, lấy element qua @ViewChild:

@ViewChild('viewer') viewerRef!: ElementRef<PgsMapViewerElement>;
search(plate: string) { this.viewerRef.nativeElement.instance?.search(plate); }

Cấu hình phức tạp (token dạng hàm refresh, theme, messages, endpoints...) gán qua property options trước khi element được gắn vào DOM, hoặc gọi el.reload() sau khi gán:

el.options = {
  token: () => this.auth.getAccessToken(),
  router: { accessibleBy: "PEDESTRIAN" },
};

| Attribute | Ý nghĩa | | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | api-url | URL gốc API (bắt buộc) | | token | Access token | | map-ids | Chỉ hiển thị các tầng này (id cách nhau dấu phẩy) | | map-id | Tầng hiển thị đầu tiên | | stomp-url · stomp-login · stomp-passcode · stomp-destination | Real-time qua RabbitMQ Web-STOMP (xem mục Real-time) | | origin | Điểm xuất phát: id device Kiosk trên layout, hoặc mapId,x,y (pixel ảnh map) | | poll-interval | Chu kỳ (ms) tự tải lại trạng thái ô (dự phòng khi không dùng STOMP) | | pitch | 45 để mở ở chế độ 2.5D | | show-edges · hide-devices | Vẽ cạnh + node đồ thị (kiểm tra layout) · ẩn node device (kiosk, thang máy…) | | no-fade-controls | Thanh tầng + nút zoom / xoay luôn hiện rõ (mặc định mờ, rê chuột vào thì rõ, rời chuột 2s thì mờ lại — fadeControls) | | floors-collapsed | Thanh tầng thu gọn khi mở (chỉ còn tầng đang xem). Không khai báo: nhớ lựa chọn lần trước | | show-roads | Vẽ mạng đường đi thành đường nền mờ kiểu Google Maps (nét rộng màu sáng, dưới ô đỗ) | | device-kinds | Chỉ hiện các nhóm thiết bị này, vd kiosk,elevator,stairs | | hide-zcu | Ẩn ZCU / bộ điều khiển (icon, vùng, tên) — web khách hàng | | route-on-click | Click ô đỗ thì chỉ đường tới ô đó | | no-slot-detail | Tắt card chi tiết khi click ô đỗ | | slot-detail-position | Neo card chi tiết ở góc: top-left · top-right · bottom-left · bottom-right. Mặc định: nằm ngay dưới ô tìm kiếm (điện thoại: bottom sheet) | | no-route-arrows | Tắt mũi tên hướng đi dọc đường chỉ dẫn (mặc định bật) | | no-search · no-stats | Ẩn ô tìm xe / chip thống kê chỗ trống (ui.search / ui.stats = false). Dùng cho web khách hàng mở từ QR | | device-status | Hiện trạng thái kết nối thiết bị; giá trị là chu kỳ làm mới (ms), bỏ trống = 15000 | | no-origin-picker | Không hiện khung chọn điểm xuất phát khi chỉ đường mà chưa có vị trí | | gps | Định vị GPS trên điện thoại. Để trống = mặc định, hoặc JSON GpsOptions (xem Chỉ đường trên điện thoại) | | guide-from-url | Trang mở từ QR ở Kiosk: đọc ?kiosk=&slot= làm điểm xuất phát và ô đích | | coordinates, meters-per-pixel, image-origin | Hệ toạ độ và ảnh nền (xem Hệ toạ độ) |

DOM event (payload trong event.detail): pgs-ready, pgs-error, pgs-mapchange, pgs-slotclick, pgs-slotdetail, pgs-slotchange, pgs-route, pgs-stats, pgs-themechange, pgs-realtime, pgs-gps, pgs-gpsstate, pgs-arrive, pgs-guidelink.

2. Class JS

import { MapViewer } from "@kztek/sdk-pgs";

const viewer = new MapViewer({
  container: document.getElementById("map")!,
  apiUrl: "https://parking.example.com",
  token: () => auth.getAccessToken(), // string hoặc hàm (sync/async)
  origin: { deviceId: KIOSK_DEVICE_ID }, // Kiosk đã đặt trên layout
  pollInterval: 5000, // tự cập nhật trạng thái ô mỗi 5s
});

await viewer.ready;
viewer.on("route", (r) => console.log(r?.instructions));
await viewer.search("30A12345"); // tìm xe → chỉ đường
viewer.destroy(); // gọi trong ngOnDestroy

3. <script> thuần (không dùng bundler)

<link
  rel="stylesheet"
  href="https://unpkg.com/maplibre-gl/dist/maplibre-gl.css"
/>
<script src="node_modules/@kztek/sdk-pgs/dist/index.umd.js"></script>
<!-- đã bundle sẵn maplibre-gl -->
<script>
  KztekPGS.defineElements();
</script>

Xem examples/viewer.html.


API MapViewer

| Method | Mô tả | | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ready: Promise<void> | Xong khi đã tải danh sách tầng và layout tầng hiển thị đầu tiên | | search(query) | Tìm theo biển số hoặc mã ô (tìm ở dữ liệu đã tải trước, không thấy thì gọi API), rồi chỉ đường | | navigateToSlot(slot \| id) | Chỉ đường tới một ô đỗ | | routeBetween(from, to, label?) | Chỉ đường giữa 2 điểm: { deviceId } / { nodeId } / { mapId, coordinates } | | nextLeg() · clearRoute() · getRoute() | Điều khiển chặng đang chỉ dẫn | | setOrigin(o) · setOriginFromQr(text) · getOrigin() | Đặt điểm xuất phát: { deviceId } hoặc { mapId, coordinates } (pixel ảnh map) | | requestRoute(slot) · openPlanner(slot) · closePlanner() · clearOrigin() | Chỉ đường như khi người dùng bấm (mở bảng "từ → đến" với ô chọn Kiosk) / mở, đóng bảng / bỏ điểm xuất phát | | startPolling(ms) · stopPolling() | Bật / tắt tự tải lại trạng thái ô | | showMap(mapId) / setFloor(mapId) | Đổi tầng | | showSlotDetail(slotOrId) · closeSlotDetail() | Gọi API chi tiết ô và mở / đóng card ở góc màn hình (trạng thái, biển số, ảnh chụp, nút Chỉ đường). Click ra ngoài ô cũng đóng card | | applySlotEvent(ev) | Đẩy sự kiện thủ công (khi app tự nhận qua MQTT hoặc SignalR) | | getStats(mapId?) · getSlots(mapId?) | Thống kê / danh sách ô đỗ | | toggleView(pitch?) | Chuyển 2D ⇄ 2.5D | | setRoadsVisible(visible) | Bật / tắt đường nền mờ (showRoads): mạng đường đi vẽ kiểu Google Maps — nét rộng, màu sáng, viền nhạt, dưới ô đỗ và đường chỉ dẫn | | setAreasVisible(visible) · getAreas(mapId?) | Bật / tắt lớp khu vực (showAreas, mặc định bật; Web Component: hide-areas) · danh sách khu vực vẽ trên tầng ({ id, name, code, limit, enabled, points, color }) | | storageEndpoint (option) · setStorageEndpoint(value) | Nơi lấy link ảnh nền map / ảnh chụp ô: gửi ?storageEndpoint=public|local cho API tìm map, layout map, tìm ô, chi tiết ô. Không đặt = mặc định backend. setStorageEndpoint() đổi lúc chạy và tải lại dữ liệu | | setAreaNamesVisible(visible) | Hiện / ẩn tên khu vực vẽ ở giữa vùng (option areaNames, mặc định hiện; Web Component: hide-area-names). Thẻ thống kê không đổi | | loadAreaStats(mapId?) · getAreaStatistics(mapId?) · fitArea(areaId) | Thẻ thống kê khu vực (areaStats, mặc định bật; Web Component: no-area-stats): mỗi khu vực một thẻ màu theo khu vực — tên, số xe / sức chứa · %, thanh lấp đầy (xanh < 70%, cam ≥ 70%, đỏ sẫm ≥ 90%) — có đường chỉ xuống vùng; bấm thẻ để zoom vào khu vực. Dữ liệu GET /statistics/maps/{id}/areas; sức chứa = limit, không có thì occupied + available. Tải bằng API khi mở tầng, refresh() và khi kết nối lại realtime; sau đó cập nhật theo realtime exchange pgs.application.area.changed (routing key all, AreaChangedEvent { areaId, name, code, limit, occupied, available, disconnected, changedUtc }; thẻ hiện thêm dòng "N mất kết nối" khi có ô mất kết nối, event cũ hơn bị bỏ qua; đổi destination: realtime.areaDestination, false = tắt). Không có realtime thì đặt areaStatsInterval (ms) để gọi lại API định kỳ (mặc định 0 = tắt). Sự kiện areastats, areachange; gọi tay: applyAreaEvent(ev) | | setEdgesVisible(visible) | Bật / tắt lớp kiểm tra layout (cạnh, waypoint, icon chỉ chiều). Khi đang chỉ đường, icon / waypoint nằm trên tuyến được vẽ bằng màu đường (theme.route), mũi tên quay theo hướng đi | | fitView(animate?, mode?) · fitMode | Fit lại tầng đang xem: 'contain' · 'cover' · 'image' · 'content'; bỏ trống mode thì giữ kiểu đang dùng | | refresh(options?) | Tải lại dữ liệu các tầng đã mở: trạng thái / biển số ô đỗ, thiết bị, mạng đường đi. Không tải lại ảnh nền, giữ khung nhìn, thẻ thông tin và đường đang chỉ. { maps: true }: tải lại cả danh sách tầng | | ensureLayout(mapId) · isFloorLoaded(mapId) | Tải trước layout một tầng / kiểm tra đã tải chưa | | locateSlot(id) | Tìm ô theo id ở mọi tầng: chưa tải thì hỏi GET /slots/{id} lấy tầng rồi tải layout tầng đó | | startGps() · stopGps(restoreOrigin?) · isGpsActive() · getGpsFix() | Bật / tắt định vị GPS (xem Chỉ đường trên điện thoại) | | getGuideLink(url?) | Link cho QR ở Kiosk: url?kiosk=<Kiosk>&slot=<ô đích> | | resize() · destroy() | | | map | Instance MapLibre gốc, dùng khi cần thêm layer riêng |

Nội dung mã QR cho setOriginFromQr: chuỗi id device, {"deviceId":"…"}, {"mapId":"…","x":120,"y":340} hoặc mapId,x,y.


API sử dụng (pgs-service)

SDK dùng các API của pgs-service (parking-v8-backend/pgs-service/docs/api.md), chỉ đọc. Token gửi qua header Authorization: Bearer <token> (IdentityServer, scope pgs-data).

Endpoint mặc định không có tiền tố /pgs (/maps/search, /slots/{id}…), ghép thẳng sau apiUrl. Nếu gateway vẫn đặt API dưới /pgs, khai báo tiền tố trong apiUrl (vd apiUrl: 'https://parking.example.com/pgs') hoặc override từng endpoint qua endpoints.

| API | Endpoint mặc định (endpoints.*) | Dùng để | | ----------------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | POST /maps/search | mapSearch | Danh sách tầng (paging: false, filter tuỳ chọn qua mapFilter) | | GET /maps/{id}/layout | mapLayout | Gọi khi cần (xem Tải layout theo tầng): map, imageUrl (presigned ảnh nền), slots[] (ô đặt trên map: vị trí + name, description, status, plateNumber, enabled), devices[] (vị trí device + type, connected, lastHeartbeatUtc, description, chỉ hiển thị), waypoints[] (nút đường đi), edges[] (nối waypoint cùng tầng), connectors[] (map connector nối waypoint sang tầng khác) | | POST /slots/search | slotSearch | Tìm xe khi chưa có trong dữ liệu đã tải: filter PlateNumber / Code eq (thử nguyên văn, viết hoa, bỏ - .) | | GET /slots/{id} | slotDetail | Card chi tiết khi click ô đỗ |

  • Tải layout theo tầng (lazy): lúc mở chỉ gọi layout của tầng hiển thị đầu tiên (mapId, tầng của Kiosk origin, hoặc tầng đầu danh sách). Các tầng khác chỉ gọi khi:

    • người dùng bấm chọn tầng (nút tầng hiện vòng xoay trong lúc tải);
    • chỉ đường xuyên tầng: SDK tải các tầng nối được từ tầng xuất phát qua map connector (connectors[] của layout);
    • tìm xe ra ô ở tầng chưa tải: SDK lấy tầng theo khu vực của slot (area.floorDevice, khớp floorDevice của map) rồi tải layout tầng đó;
    • Kiosk / cột mốc (deviceId) chưa thấy ở tầng đã tải: SDK tải lần lượt từng tầng còn lại tới khi thấy.

    Mỗi tầng chỉ gọi layout một lần (polling / refresh() mới gọi lại). Số chỗ trống trên nút tầng chỉ có sau khi tầng đó đã tải.

  • Định dạng: response camelCase (floorDeviceId, plateNumber, startId, startMapId…). SDK tự chuyển sang snake_case nội bộ (MapInfo, SlotProperties, MapNode, MapEdge).

  • Enum: backend (Newtonsoft, chưa có StringEnumConverter) trả status dạng số theo thứ tự khai báo C# (DISCONNECTED = 0, EMPTY = 1, OCCUPIED = 2). SDK đổi sang tên; backend trả chuỗi thì đọc thẳng. Số phụ thuộc thứ tự enum → backend đang chạy khác bản docs (vd bản cũ EMPTY = 0, OCCUPIED = 1) thì ô hiện sai màu; khi đó truyền slotStatusOrder: ['EMPTY', 'OCCUPIED'] (hoặc gọi setSlotStatusOrder). Khuyến nghị backend trả enum dạng chuỗi (đúng docs).

  • Slot độc lập với map: slot không có mapId; vị trí trên map nằm trong slots[] của layout (map_slots). Một slot đặt ở nhiều map thì SDK dùng vị trí ở map được tải sau cùng khi tìm / chỉ đường.

  • Search: gửi { paging: false, filter }. filter là JSON theo PSI8 BaseCriteria: { queryKey, queryType, queryValue, operation } hoặc { "or": [...] }. Helper filterOf() được export sẵn.

  • Lọc tầng: map không còn gắn với bãi xe. Dùng mapIds: [...] (lọc ở client) hoặc mapFilter (gửi lên search), ví dụ { queryKey: 'FloorDeviceId', queryType: 'GUID', queryValue: id, operation: 'eq' }.

  • Ảnh (presigned URL): entity có ảnh trên S3 trả presignedUrl, không trả objectKey:

    • ảnh nền tầng lấy imageUrl của layout, thiếu thì dùng map.presignedUrl;
    • ảnh chụp ô đỗ trên card chi tiết lấy presignedUrl của GET /slots/{id}. Không có thì card hiện "Chưa có ảnh".
  • Slot ↔ khu vực ↔ tầng: slot thuộc khu vực (area), tầng của slot là tầng của khu vực. GET /slots/{id} / search trả zcuDevice và area ({ id, name, code, floorDevice }); map trả floorDevice. SDK giữ object (zcu_device, area) và điền id phẳng zcu_device_id, area_id, floor_device_id (= area.floorDevice.id, khớp floor_device_id của map để biết ô nằm ở tầng nào). Backend cũ trả floorDevice thẳng trên slot vẫn đọc được.

  • CORS: gọi trực tiếp từ trình duyệt thì pgs-service (hoặc gateway phía trước) phải cho phép origin của web app.

Tầng: mỗi bản ghi maps là một tầng. Thứ tự tầng và hướng lên/xuống lấy từ level nếu có; nếu không, SDK suy ra từ code (MAP-B2/B2 → -2, B1 → -1, G → 0, MAP-L1/F1/T1 → 1).

Trạng thái ô: EMPTY (xanh, chỗ trống) · OCCUPIED (đỏ trầm, kèm biển số; không có biển số = có xe nhưng chưa đọc được biển) · DISCONNECTED (xám đậm theme.slotDisconnected, mất kết nối cảm biến / camera — không tính là chỗ trống) · MAINTENANCE (xám, khi slot enabled = false). Số chỗ trống = số ô EMPTY.


Yêu cầu với layout (dữ liệu vẽ trên trang quản trị)

Layout lưu qua PUT /maps/{id}/layout (ngoài SDK). Để SDK hiển thị và chỉ đường đúng:

| Dữ liệu | Yêu cầu | | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Slot | Đặt ô lên map qua slots[] của layout (x, y tâm, width × height trước khi xoay, rotation độ chiều kim đồng hồ). Thiếu kích thước thì dùng DEFAULT_SLOT_SIZE 60 × 120. Tâm ô là đích khi chỉ đường. Layout chỉ trả name (mã đầy đủ code lấy khi mở card chi tiết qua GET /slots/{id}), nhãn trên ô hiển thị name. Ô chưa đặt lên map thì không có trong layout; tìm qua API ra ô đó sẽ báo "chưa được đặt trên bản đồ" | | Device (Kiosk, thang máy, ZCU, cột mốc QR) | Đặt lên map qua devices[] — chỉ để hiển thị, không nằm trong đồ thị. Dùng id device làm điểm xuất phát (origin: { deviceId }); SDK nối vuông góc từ device về đoạn đường gần nhất. Device có width × height được vẽ thêm khung chữ nhật (xoay theo rotation). Đặt thang máy cạnh waypoint chuyển tầng để câu hướng dẫn lấy được tên thang | | Thang máy / thang bộ / ram dốc | Mỗi tầng một waypoint tại thang (nối vào đường đi của tầng) + một node DEVICE cạnh đó (đặt tên, vì tên hiện trong câu hướng dẫn). Nối waypoint thang của hai tầng bằng map connector (POST /maps/connectors, bidirectional để đi được hai chiều). Layout trả connectors[]; SDK lấy connector đi ra từ tầng làm cạnh nối tầng | | Đường đi (waypoints + edges) | Đặt waypoint dọc tim lối xe / lối đi bộ và nối bằng cạnh. Rẽ góc = nhiều waypoint nối tiếp; ngã ba, ngã tư = waypoint có ≥ 3 cạnh. Chuyển tầng: một waypoint tại thang ở mỗi tầng, nối hai tầng bằng map connector (POST /maps/connectors). Không nối cạnh tới ô đỗ hay device |

  • Ô đỗ và device được SDK nối bằng đoạn ảo vuông góc tới đoạn đường gần nhất cùng tầng.
  • Cạnh có hướng (startId → endId): đường hai chiều phải khai báo 2 cạnh ngược nhau; lối một chiều chỉ 1 cạnh. Cạnh nối tầng cũng có hướng: muốn đi cả lên lẫn xuống thì khai báo cả hai chiều. Bật "Hiện cạnh & node" (showEdges): mỗi cạnh có một icon chỉ chiều ở giữa — > khi một chiều, < > khi có cặp cạnh ngược nhau (đường hai chiều được vẽ gộp thành một đường). Màu theo theme.edge, viền mảnh theo theme.surface.

Hệ toạ độ & ảnh nền

Theo docs (mục 6): x, y, width, height của node là pixel trên ảnh map, gốc (0,0) ở góc trên trái, y hướng xuống, x, y là tâm đối tượng. rotation xoay hình chữ nhật quanh tâm, đơn vị độ, chiều kim đồng hồ. SDK dùng rotation để vẽ ô đỗ và khung thiết bị. Đây là chế độ mặc định (coordinates: 'pixel').

SDK đặt ảnh nền (presigned imageUrl của layout, kích thước width × height) lên bản đồ tại imageOrigin với tỉ lệ metersPerPixel (số mét ngoài đời trên 1 pixel ảnh). Mọi tầng dùng chung gốc; tỉ lệ lấy theo từng map — trường metersPerPixel của entity Map (response /maps/search, /maps/{id}/layout), không có thì dùng option chung:

new MapViewer({
  ...,
  metersPerPixel: 0.05,     // tỉ lệ mặc định khi map chưa có metersPerPixel: 1 px = 5 cm
  // baseImage: false,      // không vẽ ảnh nền, chỉ vẽ ô đỗ
  // coordinates: 'lnglat', // khi dữ liệu đã là [lng, lat]
});

Tỉ lệ đúng thực tế quan trọng khi chỉ đường: quãng đường & thời gian đi bộ trên thẻ hướng dẫn, khoảng cách GPS / mỗi bước chân được dời trên bản đồ, bán kính hút vào đường (snapRadius) và báo tới nơi (arriveRadius) đều tính bằng mét qua tỉ lệ này. Cách đo: chọn 2 điểm trên ảnh có khoảng cách thật đã biết (vd chiều dài một ô đỗ 5 m, hoặc một cạnh cột – cột), metersPerPixel = khoảng cách thật (m) / khoảng cách trên ảnh (px).

imageCorners: (map) => [TL, TR, BR, BL] cho phép tự đặt 4 góc ảnh nếu cần.

Ảnh nền lớn được thu nhỏ khi hiển thị: ảnh có cạnh dài > maxImageSize (mặc định 4096 px) được SDK tải về, giải mã ngoài luồng chính và thu nhỏ trước khi đưa lên bản đồ (dạng canvas) — ảnh 12000×7000 px từ ~336 MB bộ nhớ đồ hoạ còn ~37 MB, kéo / zoom / đổi tầng mượt hơn, không vượt giới hạn texture GPU của máy yếu. Màu nền theo góc ảnh lấy luôn từ ảnh đã tải (không tốn thêm lần tải). Cần S3 bật CORS (lỗi thì tự dùng ảnh gốc); maxImageSize: 0 để giữ ảnh gốc. Dung lượng tải không đổi — muốn tải nhanh hơn, backend nên lưu thêm bản ảnh tối ưu (vd WebP, cạnh dài 4096 px) và trả URL bản đó.

Ảnh nền chỉ tải một lần mỗi tầng: ảnh bản vẽ có thể nặng chục MB. SDK giữ riêng một lớp ảnh cho mỗi tầng đã mở — đổi tầng chỉ ẩn / hiện, quay lại tầng cũ hiện ngay; chọn ô, chỉ đường, polling, real-time không tải lại ảnh. Chỉ tải lại khi ảnh của tầng thật sự đổi (đường dẫn file / vị trí khác, vd sau refresh()); presigned URL đổi chữ ký mỗi lần gọi API vẫn coi là cùng ảnh.

Khung nhìn (bản đồ đầy khung):

  • fit — khung nhìn khi mở / đổi tầng:
    • 'contain' (mặc định): thấy trọn ảnh nền, phóng to hết cỡ container (chạm khít một chiều, giống CSS object-fit: contain), căn giữa. Thẻ nổi nằm đè lên bản đồ, không chừa lề. Không có ảnh nền thì theo vùng dữ liệu;
    • 'cover': ảnh nền phủ kín container, phần dư theo một chiều bị cắt, kéo để xem;
    • 'content': thấy trọn vùng có dữ liệu (ô đỗ, thiết bị, đường đi), chừa chỗ cho thẻ nổi;
    • 'image': thấy trọn ảnh nền, chừa chỗ cho thẻ nổi (lề chọn tự động: chừa bề ngang hoặc chiều cao thẻ tìm kiếm, tuỳ cách nào cho bản đồ to hơn).
  • Nút Xem toàn bộ tầng (nhóm công cụ dưới thanh tầng) hoặc viewer.fitView() quay về khung nhìn ban đầu sau khi đã kéo / zoom. viewer.fitView(true, 'cover') để đổi kiểu bằng code.
  • matchImageBackground (mặc định false): nền bản đồ lấy theo màu góc ảnh nền, để ảnh liền với khung khi zoom ra. Phải tải ảnh thêm một lần (chế độ CORS không dùng chung cache) — ảnh bản vẽ nặng thì để tắt và đặt theme.mapBackground trùng màu nền ảnh (vd #FFFFFF). Cần S3 bật CORS; không đọc được thì giữ theme.mapBackground.
  • Container đổi kích thước (đóng/mở sidebar, xoay màn hình, container lúc tạo còn ẩn) thì tự fit lại, trừ khi người dùng đã tự kéo / zoom.

Chỉ đường

Router chạy trên đồ thị của layout:

  • Mạng đường đi là waypoints nối bằng edges có hướng (startId → endId, chi phí = weight); map connector (connectors[] của layout, có hướng) nối waypoint sang tầng khác. Đặt router.directed: false nếu muốn coi mọi cạnh là hai chiều. Không có đường đi theo đúng chiều cạnh thì SDK báo "Không tìm được đường đi" (không kẻ đoạn ảo băng qua). Tên điểm chuyển tầng lấy từ device gần nhất (trong router.connectorRadius, mặc định 15 m).
  • Đích là ô đỗ: SDK kẻ đoạn ảo vuông góc từ tâm ô xuống cạnh gần nhất, thêm chân đường vuông góc vào cạnh (chia weight theo tỉ lệ), rồi tìm đường tới đó.
  • Xuất phát là device Kiosk (origin: { deviceId }) hoặc toạ độ tự do ({ mapId, coordinates }, QR, chọn trên bản đồ) — đều được nối vuông góc vào đoạn đường gần nhất như ô đỗ.
  • Thuật toán: cùng tầng hay khác tầng đều chạy một lần A* (ngraph.path) trên tổng weight, nên SDK tự chọn thang tối ưu. Đi thẳng qua nhiều tầng trong một thang được gộp thành một lần chuyển tầng.
  • Kết quả:
    • các chặng theo tầng (legs, kèm nodeIds);
    • điểm chuyển tầng (transitions, tên lấy từ tên device thang);
    • câu hướng dẫn, ví dụ "Vui lòng di chuyển đến Thang máy khối A và đi xuống Hầm B2".
  • Hiển thị:
    • Đường một màu liền (theme.route) đi theo các cạnh (qua các waypoint) và đoạn ảo vuông góc vào ô, bo tròn, có bóng đổ mờ để nổi trên nền ảnh.
    • Điểm đích có ghim location kiểu Google Maps (rơi xuống, nảy nhẹ) và sóng lan ở chân ghim. Ô đích sáng/tối nhẹ.
    • Mũi tên hướng đi dọc tuyến (mặc định bật): huy hiệu tròn màu theme.route, mũi tên theme.routeArrow, viền theme.surface, đặt đều theo chiều đi và to dần khi zoom gần. Tắt bằng routeArrows: false (Web Component: no-route-arrows).
    • Thẻ hướng dẫn có nút sang tầng tiếp theo.
new MapViewer({
  ...,
  origin: { deviceId: KIOSK_DEVICE_ID },
  router: {
    // Bỏ qua cạnh (vd thang máy đang bảo trì). Cạnh enabled = false luôn bị bỏ qua
    isEdgeAvailable: (edge, mapId) => !closedWaypointIds.has(edge.start_id),
  },
  showDevices: true,   // vẽ node device (kiosk, thang máy…) kèm tên — mặc định true
  showEdges: false,    // vẽ cạnh + node đồ thị để kiểm tra layout
});

Dùng PgsRouter độc lập (ví dụ chạy ở backend Node hoặc trong unit test):

const router = new PgsRouter(maps);
router.setGraph(
  mapId,
  nodes /* { id, mapId, type, name, coord } */,
  edges /* MapEdge & { path?: LngLat[] } — path = points đã quy đổi */,
);
const route = router.route(
  { nodeId: KIOSK_ID },
  { nodeId: SLOT_ID },
  { destinationLabel: "ô B2-C15" },
);

Chỉ đường trên điện thoại: QR ở Kiosk + GPS

Luồng tìm xe:

  1. Khách tìm xe ở Kiosk. Kiosk vẽ đường đi và hiện mã QR.
  2. Khách quét QR bằng điện thoại. Link chứa id Kiosk và id ô đỗ: https://app/pgs-guide?kiosk=<deviceId>&slot=<slotId>.
  3. Điện thoại mở bản đồ, vị trí ban đầu là Kiosk vừa quét, đường đi được vẽ sẵn.
  4. Khi khách di chuyển, SDK dùng GPS để biết đã đi bao xa, theo hướng nào kể từ lúc quét. Chấm "Vị trí của bạn" chạy theo trên map 2D và đường đi được tính lại.

Cách này không cần bản đồ có toạ độ thật: GPS chỉ dùng để đo độ dời so với Kiosk (chế độ relative).

Kiosk: hiện QR

SDK không kèm thư viện vẽ QR. App truyền hàm render, ví dụ dùng qrcode:

import QRCode from 'qrcode';

new MapViewer({
  ...,
  origin: { deviceId: KIOSK_DEVICE_ID },
  qr: {
    url: 'https://parking.example.com/pgs-guide',     // trang chỉ đường trên điện thoại
    render: (link) => QRCode.toDataURL(link, { margin: 1, width: 360 }),
  },
});
  • Card QR hiện ở góc trên phải khi Kiosk đang chỉ đường tới một ô, và ẩn khi xoá chỉ đường.
  • Muốn tự vẽ QR ở chỗ khác: nghe viewer.on('guidelink', (link) => ...) (Web Component: pgs-guidelink) hoặc gọi viewer.getGuideLink(url).

Điện thoại: trang mở từ QR

<!-- Web khách hàng: link QR đã có Kiosk + xe → ẩn ô tìm xe -->
<pgs-map-viewer
  api-url="..."
  [attr.token]="token"
  guide-from-url
  gps
  no-search
></pgs-map-viewer>
new MapViewer({ ..., guideFromUrl: true, gps: true, ui: { search: false } });
  • guideFromUrl đọc ?kiosk=&slot= (hoặc &plate=): đặt điểm xuất phát là Kiosk, rồi chỉ đường tới ô.
  • gps bật định vị ngay khi chỉ đường (autoStart: 'search'). Trình duyệt hỏi quyền vị trí. Fix GPS đầu tiên được neo vào Kiosk.
  • Hướng bản đồ: mặc định lấy theo ảnh gốc — hướng "lên" của ảnh là hướng bắc. Ảnh vẽ xoay so với hướng bắc thì khai báo gps: { rotation: 30 } (số độ, chiều kim đồng hồ, từ hướng bắc tới hướng "lên" của ảnh), hoặc autoRotate: true để SDK tự tính khi khách đi được ~12 m theo đường chỉ dẫn.
  • Dẫn đường trên điện thoại (chống chóng mặt): khi không dẫn đường camera chỉ trượt lúc chấm sắp ra khỏi vùng giữa màn hình (60%); chấm vị trí chạy liền mạch với tốc độ đi bộ đo được (độ dài bước / nhịp bước), không nhảy theo từng bước; đang dẫn đường và bám vị trí thì camera trôi theo chấm từng khung hình như Google Maps; đường chỉ dẫn phía sau chấm bị cắt bỏ và quãng đường còn lại trên thẻ hướng dẫn cập nhật theo từng khung hình (~12 lần/giây), không đợi tính lại đường mỗi 2 m; chùm sáng bỏ rung < 3°; ô đích "thở" chậm thay vì nhấp nháy; bật "Giảm chuyển động" của hệ điều hành thì tắt mọi hiệu ứng. Bước chân bám theo hướng đường đang đi khi la bàn lệch ≤ 50°.
  • Chi phí đường đi tính bằng mét: weight của backend (pixel ảnh, mỗi map một tỉ lệ) được quy về mét theo tỉ lệ weight/độ dài của từng tầng, cạnh không có weight dùng độ dài thật, connector quy theo tầng đầu — tìm đường xuyên tầng có tỉ lệ khác nhau vẫn đúng. Quãng đường / thời gian đi bộ hiển thị tính từ toạ độ (đúng thực tế khi map có metersPerPixel).
  • Đếm bước chân (trong hầm / toà nhà): GPS gần như đứng yên khi ở trong nhà, nên SDK đếm bước bằng gia tốc kế. Mỗi bước, chấm vị trí dời stepLength (0.7 m) theo hướng la bàn rồi hút vào đường chỉ dẫn. Chế độ relative: máy có cảm biến thì dùng bước chân thay cho GPS. Absolute: bước chân nối giữa các fix GPS tốt. Tắt bằng gps: { steps: false }.
  • Giảm nhiễu:
    • vị trí cách đường chỉ dẫn (hoặc mạng đường đi của tầng) dưới snapRadius (10 m) thì được hút vào đường;
    • fix có sai số trên maxAccuracy (30 m) bị bỏ qua;
    • đường chỉ được tính lại khi đi được từ minDistance (2 m) trở lên.
  • Vị trí + hướng nhìn (giống Google Maps): khi bật GPS, chấm vị trí có chùm sáng xoay theo la bàn điện thoại (hướng máy đang quay), quy về bản đồ theo hướng của ảnh; máy không có la bàn thì theo hướng di chuyển. iOS hỏi quyền "Chuyển động & hướng" khi bấm nút định vị (GPS tự bật thì hỏi ở lần chạm bản đồ đầu tiên). Tắt bằng gps: { compass: false }.
  • Đến nơi: khi cách ô đích dưới arriveRadius (5 m), SDK hiện "Bạn đã đến ô …" và phát sự kiện arrive.
  • Sang tầng: ra khỏi thang thì bấm nút "Xem B2 →". Vị trí chuyển tới cửa thang ở tầng mới và GPS được neo lại tại đó.
  • Cột mốc QR dọc đường: khi đang đi mà gọi setOriginFromQr(text) / setOrigin(), GPS được neo lại tại cột mốc, xoá sai số tích luỹ.
  • Nút định vị (dưới thanh chọn tầng): kéo bản đồ thì ngừng bám, bấm nút để bám lại; bấm khi đang bám thì tắt GPS và về lại vị trí Kiosk.

Tuỳ chọn gps

| Option | Mặc định | Ý nghĩa | | ------------------------------------------------------------------------------ | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | mode | 'auto' | 'relative': neo tại Kiosk. 'absolute': quy đổi thẳng GPS sang pixel, cần controlPoints. 'auto' chọn absolute khi có controlPoints / toPixel / coordinates: 'lnglat' | | northOffset (field của Map, backend) | null | Hướng ảnh map: độ từ hướng bắc tới phía trên ảnh, đo tại bãi bằng la bàn điện thoại. Có giá trị thì dùng cho tầng đó (ưu tiên hơn rotation / hiệu chỉnh tại Kiosk) → hướng luôn đúng dù khách đứng thế nào, mở lại trang vẫn đúng | | rotation · autoRotate | 0 · false | Góc lệch của ảnh map so với hướng bắc | | compass | true | Chùm sáng hướng nhìn theo la bàn điện thoại | | kioskFacing | true | Hiệu chỉnh hướng lúc quét QR / chọn Kiosk: điện thoại đang hướng về Kiosk → hướng la bàn lúc đó = hướng khách nhìn trên bản đồ (true = lên 12h; số độ; hoặc { [id Kiosk]: độ }), từ đó suy ra góc lệch ảnh ↔ bắc. Bỏ qua khi khai báo rotation; false = tắt | | rotateMap | true | Dẫn đường trên điện thoại: xoay bản đồ theo hướng đường (đoạn đầu hướng lên trên, tới khúc rẽ > 30° thì xoay tiếp), tên ô tự lật cho đọc xuôi. Khách tự xoay / kéo / bấm la bàn → tạm dừng, bấm nút định vị để bật lại; đóng chỉ đường → về hướng bắc | | steps · stepLength | true · 0.7 (m) | Đếm bước chân để di chuyển chấm vị trí khi GPS không đổi | | routeLock | true | Dẫn đường bám tuyến theo mốc: bước chân đẩy vị trí dọc tuyến (la bàn chỉ để biết tới / lùi), vòng sai số to dần theo quãng đường kể từ mốc; thẻ hướng dẫn chỉ từng chỗ rẽ ("Đi thẳng 12 m, rẽ trái tại Kiosk 2"); tới gần Kiosk / thang trên tuyến hiện nút "Bạn đang ở …?", bấm (hoặc quét QR, hoặc bấm "Tôi đang ở đây" trong popup thiết bị) để đặt lại vị trí chính xác. viewer.checkIn(deviceId) làm việc tương tự | | stepSnapRadius | 2 (m) | Bán kính hút vị trí đếm bước vào đường đã vẽ (GPS dùng snapRadius). Nhỏ để đi ngang / ra khỏi đường vẫn thấy chấm di chuyển | | debug | false | Bảng chẩn đoán góc dưới trái: trạng thái GPS / sai số, la bàn (độ thật → độ trên map), hướng map (northOffset), số bước, cảm biến, tỉ lệ | | autoStart | 'search' | true: bật ngay khi bản đồ sẵn sàng · 'search': lần đầu chỉ đường · false: chỉ khi bấm nút | | mobileOnly | true | Chỉ chạy trên điện thoại / máy tính bảng (nhận theo user agent, nên màn hình cảm ứng của Kiosk không bị tính) | | maxAccuracy · minDistance · snapRadius · arriveRadius · boundsMargin | 30 · 2 · 10 · 5 · 30 (m) | Lọc nhiễu · tính lại đường · hút vào đường · báo tới nơi · coi là ngoài bãi | | follow | true | Camera bám theo vị trí | | controlPoints · mapControlPoints · transform · toPixel · floor | | Chỉ dùng cho chế độ absolute (khi ảnh có toạ độ thật) |

Sự kiện:

  • gps: mỗi fix, kèm status (anchored · ok · inaccurate · outside · no-anchor…);
  • gpsstate: off · locating · tracking · error;
  • arrive, guidelink.

Lưu ý: trang trên điện thoại phải chạy HTTPS thì trình duyệt mới cho dùng GPS. GPS chính xác ở bãi ngoài trời / tầng mái, sai số thường 3–10 m. Trong hầm có thể mất tín hiệu, khi đó chấm vị trí đứng yên ở fix tốt gần nhất. Nên đặt thêm cột mốc QR dọc lối đi để khách quét, neo lại vị trí.


Bảng chỉ đường: từ đâu → đến đâu

Khi người dùng chỉ đường (tìm xe, chọn gợi ý, nút Chỉ đường trên thẻ thông tin xe, link QR), cột trái hiện bảng "từ đâu → đến đâu" kiểu Google Maps, thay chỗ ô tìm kiếm. Bảng hiện suốt lúc chỉ đường, thẻ thông tin xe vẫn giữ bên dưới:

Chỉ đường                                 ×   ← đóng bảng & xoá đường (thẻ xe vẫn giữ)
○  [ Kiosk T1-1                    ▾  × ]     ← ô chọn Kiosk (select) · × bỏ điểm xuất phát
⋮
📍 [ ô đỗ A040 · 30A-00035 · Tầng 1    ]
  • Dòng xuất phát là ô chọn Kiosk (<select> gốc của trình duyệt — trên điện thoại mở bảng chọn của hệ điều hành), nhóm theo tầng bằng <optgroup>. Chọn Kiosk là đặt điểm xuất phát và vẽ đường ngay; chọn Kiosk khác để đổi.
  • Chưa có điểm xuất phát (không có Kiosk origin, GPS không bật / bị từ chối): tiêu đề là "Bạn đang ở đâu?", ô chọn hiện "Chọn Kiosk bạn đang đứng". Khi mở bảng, SDK tải layout các tầng còn lại để liệt kê đủ Kiosk.
  • Điểm xuất phát không phải Kiosk (vị trí GPS, toạ độ đặt bằng setOrigin) hiện thành lựa chọn đầu tiên trong ô, vd "Vị trí của bạn (GPS)".
  • Bỏ điểm xuất phát: nút × trong ô chọn — xoá đường, chờ chọn Kiosk khác. Gọi bằng code: viewer.clearOrigin().
  • Kết thúc chỉ đường: nút × trên tiêu đề bảng (viewer.clearRoute()); ô tìm kiếm hiện lại.
  • Click ô đỗ khi bật routeOnSlotClick (trang quản trị) không mở bảng, chỉ làm nổi bật ô.
new MapViewer({
  ...,
  originPicker: true,                                   // mặc định; false = không hiện bảng
  originDevices: (d) => d.name.startsWith('Kiosk'),   // hoặc mảng id device
});

Danh sách Kiosk mặc định: device có tên chứa "Kiosk"; không có device nào như vậy thì liệt kê mọi device có tên.


Thiết bị: icon theo loại & trạng thái kết nối

Thiết bị đặt trên layout (devices[]) hiển thị thành ghim location (giọt nước, kiểu Google Maps — HTML marker, sắc nét). Mũi ghim chạm đúng vị trí thiết bị, đầu ghim màu nhóm có biểu tượng trắng:

  • Mốc định hướng của khách — Kiosk, thang máy, thang bộ: ghim lớn, tên luôn hiện cạnh phải đầu ghim (chữ cùng màu ghim, viền sáng). Zoom xa (< 18) ẩn tên.
  • Thiết bị kỹ thuật — ZCU, camera, LED, cổng / làn: icon tròn nhỏ ở tâm vùng thiết bị, tên ngay bên dưới (không dùng ghim). Zoom xa (< 18) ẩn tên.
  • Ẩn ZCU cho web khách hàng: hideZcu: true (Web Component: hide-zcu) — ẩn cả icon, vùng và tên của ZCU / bộ điều khiển.
  • Bấm vào thiết bị: popup tên · loại (· trạng thái kết nối nếu bật deviceStatus).

| Nhóm | Biểu tượng | Màu mặc định (theme.deviceColors) | Nguồn | | ------------ | ------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------- | | kiosk | màn hình trên chân đế | #F05922 (cam nhấn) | DeviceType KIOSK | | elevator | cabin + mũi tên lên/xuống | #251C53 | TRANSPORT với typeCode = 0 (hoặc không có typeCode); tên chứa "thang máy" / "elevator" / "lift" | | escalator | bậc chéo + tay vịn | #251C53 | TRANSPORT với typeCode = 1; tên chứa "thang cuốn" / "escalator" | | stairs | bậc thang | #251C53 | tên chứa "thang bộ" / "stair" | | controller | chip điều khiển | #6E6A86 | ZCU, CONTROL_UNIT, COMPUTER | | camera | máy ảnh | #6E6A86 | CAMERA, PGS_CAMERA | | display | bảng LED | #6E6A86 | LED, PGS_LED | | gate | cần barrier | #6E6A86 | GATE, LANE, DOOR | | floor | các lớp tầng | #4A3F8C | FLOOR | | generic | chấm tròn | #6E6A86 | còn lại |

  • Loại thang (TRANSPORT): thuộc tính typeCode của device — 0 thang máy, 1 thang cuốn. SDK đọc typeCode (field phẳng) hoặc attributes ([{ code, value }] / { typeCode }) trong devices[] của layout; layout chưa trả thì đoán theo tên, mặc định thang máy.
  • Loại thiết bị: type (DeviceType) trong devices[] của layout; thiếu thì đoán theo tên ("Kiosk…", "ZCU…", "Camera…"). Thang máy / thang bộ không có trong DeviceType nên luôn suy từ tên.
  • Lọc nhóm hiển thị: showDevices: ['kiosk', 'elevator', 'stairs'] (Web Component: device-kinds="kiosk,elevator,stairs") — vd web khách hàng chỉ cần mốc định hướng, ẩn ZCU / camera. showDevices: false (hide-devices) ẩn hết.
  • Vùng thiết bị: thiết bị có width × height được vẽ thêm vùng chữ nhật (đã xoay theo rotation) cùng màu ghim — nền nhạt, viền liền. Tắt bằng showDeviceArea: false. Lọc showDevices theo nhóm thì vùng của nhóm bị ẩn cũng ẩn.
  • Danh sách Kiosk trong bảng chỉ đường mặc định lấy thiết bị loại KIOSK.

Trạng thái kết nối (heartbeat) — bật khi tạo view:

new MapViewer({ ..., deviceStatus: true });                  // làm mới mỗi 15s
new MapViewer({ ..., deviceStatus: { interval: 10000 } });   // chu kỳ riêng (tối thiểu 5s)
<pgs-map-viewer api-url="..." device-status></pgs-map-viewer>
<pgs-map-viewer api-url="..." device-status="10000"></pgs-map-viewer>
  • Thang nối tầng: thang có map connector (connectors[] của layout, startDeviceId / endDeviceId = thang) hiện huy hiệu ⇅ ở chân ghim. Bấm vào thang → popup có mục Đi tới với từng tầng nối tới (↑ lên / ↓ xuống theo thứ tự tầng); bấm tầng → sang tầng đó, bay tới thang ở đầu bên kia và mở popup của thang đó. Connector đang tắt (enabled = false) không hiện.
  • Thang (máy / cuốn / bộ) không hiện trạng thái kết nối (không chấm, không dòng trạng thái trong popup).
  • Chấm ở góc trên đầu ghim: xanh = đang kết nối (theme.deviceOnline), đỏ = mất kết nối (theme.deviceOffline); thiết bị mất kết nối thì ghim nhạt màu. Chưa có trạng thái thì không hiện chấm.
  • Popup hiện "Đang kết nối" / "Mất kết nối", dòng dưới là description của device trong layout (message của heartbeat gần nhất, vd "OK", "Ping timeout …", hoặc "Heartbeat timeout" khi PGS tự đánh dấu mất kết nối); không có thì hiện thời điểm tín hiệu cuối (lastHeartbeatUtc).
  • Chỉ làm mới thiết bị của tầng đang xem; tab trình duyệt ẩn thì tạm dừng.
  • Nguồn dữ liệu: connected / lastHeartbeatUtc trong devices[] của GET /maps/{id}/layout — không gọi API riêng. Bật deviceStatus thì SDK tải lại layout tầng đang xem mỗi interval (cập nhật luôn trạng thái ô đỗ); pollInterval cũng cập nhật trạng thái thiết bị.
  • API: viewer.refreshDeviceStatuses(mapIds?) (tải lại layout các tầng), viewer.getDeviceStatus(id); sự kiện devicestatus (Web Component: pgs-devicestatus). PgsApiClient.getDeviceStatuses(ids) (POST /devices/status) vẫn còn cho app cần dùng riêng, SDK không gọi.

Real-time (RabbitMQ Web-STOMP)

SDK dùng @stomp/rx-stomp (^2.0.0, như các web app khác) để subscribe exchange sự kiện slot, rồi cập nhật màu, biển số và card chi tiết ngay khi có xe vào/ra.

// Cách 1: dùng chung kết nối STOMP sẵn có của app (khuyến nghị cho Angular: StompService extends RxStomp).
// SDK chỉ subscribe, không activate/deactivate kết nối của app.
new MapViewer({ ..., realtime: { stomp: this.stompService } });

// Cách 2: SDK tự kết nối
new MapViewer({
  ...,
  realtime: {
    brokerURL: 'ws://192.168.21.46:15674/ws',
    login: 'guest',
    passcode: 'guest',
    destination: '/exchange/pgs.application.event.slot.changed/all',   // mặc định, có thể truyền mảng
  },
});
<pgs-map-viewer
  stomp-url="ws://192.168.21.46:15674/ws"
  stomp-login="guest"
  stomp-passcode="guest"
></pgs-map-viewer>
  • Destination mặc định: /exchange/pgs.application.slot.changed/all (DEFAULT_SLOT_DESTINATION) — SlotChangedEvent của pgs-service, publish sau mỗi lần PUT /slots/{id}/occupancy thực sự đổi ô (xe vào, xe rời, đổi biển số).
  • Message nhận được: SlotChangedEvent { slotId, status, plateNumber, areaId, floorDeviceId, zcuDeviceId, changedUtc } (camelCase hoặc PascalCase). Cũng nhận SlotDto ({ id, status, plateNumber }), mảng, bọc { data }. Định dạng khác thì truyền realtime.parse(body, message).
  • Xử lý: tìm ô theo slotId trong các tầng đã tải (không có thì bỏ qua); đổi màu, biển số, số chỗ trống (cả trên nút tầng của tầng khác), thẻ thông tin xe đang mở tự tải lại (ảnh mới). Event có changedUtc cũ hơn lần cập nhật gần nhất của ô bị bỏ qua. Event không có trạng thái cũ → slotchange.event.status_from là trạng thái đang hiển thị trước khi đổi (app biết xe vào / xe rời).
  • Biển số: message có plateNumber / plateNumberTo = null sẽ xoá biển số (xe ra). Message không có key biển số thì giữ nguyên biển số đang hiển thị.
  • Kết nối lại: khi rớt rồi kết nối lại, SDK tự tải lại layout tầng đang xem một lần để bù các sự kiện bị lỡ.
  • Sự kiện trạng thái kết nối: viewer.on('realtime', (s) => ...) với s là 'open' hoặc 'close'. Với Web Component là DOM event pgs-realtime.
  • Dùng song song với polling: có thể bật thêm pollInterval làm dự phòng.

Theme (cấu hình mã màu)

Client tự cấu hình mã màu cho giao diện và bản đồ. Chỉ cần khai báo các mã muốn đổi; mã không khai báo dùng giá trị mặc định (bộ màu KZTEK). Đổi được lúc khởi tạo hoặc lúc đang chạy; bản đồ đổi màu ngay, không tải lại.

Thứ tự áp dụng (lớp sau ghi đè lớp trước): mặc định → biến CSS --pgs-* trên container → option theme → setTheme().

// 1. Khi khởi tạo
new MapViewer({
  ...,
  theme: {
    primary: '#003366',
    accent: '#00A3FF',
    route: '#00A3FF',
    slotEmpty: '#1B8F4A',
    slotOccupied: '#B23A3A',
    statusColors: { RESERVED: '#4A3F8C' },   // màu cho trạng thái ô thêm
  },
});

// 2. Lúc đang chạy (ví dụ lấy từ màn hình cấu hình của khách hàng)
viewer.setTheme({ slotEmpty: '#1B8F4A', routeWidth: 14 }); // chỉ đổi các mã này
viewer.setTheme({ primary: '#003366' }, { reset: true });  // các mã khác trở về mặc định
viewer.theme;                                              // bộ màu đang dùng
viewer.on('themechange', (t) => ...);
/* 3. Qua CSS của app: tên biến = --pgs- + token dạng kebab-case */
pgs-map-viewer {
  --pgs-primary: #003366;
  --pgs-slot-empty: #1b8f4a;
  --pgs-route-width: 14;
}
<!-- 4. Web Component: attribute theme-<token> hoặc theme='{JSON}'. Đổi attribute là áp ngay -->
<pgs-map-viewer
  theme-primary="#003366"
  theme-route="#00A3FF"
  theme-slot-occupied="#B23A3A"
></pgs-map-viewer>
<pgs-map-viewer
  theme='{"primary":"#003366","statusColors":{"RESERVED":"#4A3F8C"}}'
></pgs-map-viewer>

Angular binding: [attr.theme-primary]="config.primaryColor".

| Nhóm | Token (mặc định) | | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Giao diện | primary (#251C53), onPrimary (#FFFFFF, chữ trên nền primary), accent (#F05922), onAccent (#FFFFFF), accentHover (#D94E1C), accentSoft (#FFAA80), secondary (#4A3F8C), soft (#B8B3D6), hover (#FFF5EF), border (#CBCBCB), surface (#FFFFFF), text (#1E1B2E), shadow, radius (10, px), fontFamily | | Ô đỗ | slotEmpty (#2E9E5B), slotOccupied (#A93A3A), slotMaintenance (#9A9A9A), statusColors ({}), slotOpacity (0.85), slotOutline (#FFFFFF), slotLabel (#FFFFFF), slotLabelHalo | | Bản đồ & đồ thị | mapBackground (#F3F2F8), deviceColors (màu huy hiệu theo nhóm thiết bị, xem Thiết bị), deviceOnline (#2E9E5B) / deviceOffline (#A93A3A) (chấm trạng thái), device (#4A3F8C, màu dự phòng), deviceLabel (#251C53), edge (#4A3F8C, cạnh & waypoint khi bật showEdges), edgeWidth (3), road (#FFFFFF) · roadCasing · roadOpacity (0.7) (đường nền mờ showRoads), area (#4A3F8C) · areaOpacity (0.1) (khu vực không có color riêng) | | Chỉ đường | route (#F05922, màu duy nhất của đường), routeShadow (bóng đổ tối mờ dưới đường, tách khỏi nền ảnh), routeWidth (8), highlight (#F05922, ô đích nhấp nháy), pin (#F05922, ghim location ở điểm đích), origin (mặc định cùng màu route, chấm "Vị trí của bạn"), routeArrow (#FFFFFF, mũi tên trong huy hiệu hướng đi) |

Danh sách đầy đủ có sẵn qua DEFAULT_THEME, tiện để dựng form cấu hình màu.


Tuỳ biến khác

| Option | Mô tả | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | messages | Chuỗi hiển thị, override từng phần | | slotLabel | Nhãn trên ô đỗ (mặc định: description của ô nếu có, không thì name). Vd rút gọn tên dài: (s) => s.properties.name.split('-').pop() ("ZCU-CAMERA_5-CAM9-Slot_1" → "Slot_1"); trả ''/null để ẩn. Nhãn tự chạy dọc chiều dài ô (ô đứng → chữ dọc), nhãn đè nhau tự ẩn; zoom gần mới thêm biển số | | hideSlotLabels | Ẩn nhãn ô theo trạng thái, cập nhật theo realtime. Vd ['EMPTY', 'DISCONNECTED'] chỉ hiện nhãn ô có xe. Mặc định [] (hiện tất cả). Web Component: hide-slot-labels="EMPTY,DISCONNECTED"; đổi lúc chạy: viewer.setHiddenSlotLabels([...]) | | ui | Bật/tắt search, stats, floorSwitcher, guide, viewToggle, locate để tự làm UI riêng | | glyphs / fontStack | Font server cho nhãn chữ (mặc định demotiles.maplibre.org). glyphs: false để chạy offline (tắt nhãn) | | style · mapOptions | Style hoặc option MapLibre riêng | | client | Dùng chung một PgsApiClient cho nhiều màn hình