Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ jobs:
- name: Checkout
uses: actions/checkout@v5

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'

- name: Validate camera sync script
run: node --check scripts/sync-cameras.mjs

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
Expand Down
89 changes: 89 additions & 0 deletions .github/workflows/sync-cameras.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: Sync camera data

on:
schedule:
- cron: '17 */6 * * *'
workflow_dispatch:
inputs:
force_geocode:
description: Re-run VietMap reverse geocoding for every camera with coordinates
required: false
default: false
type: boolean

permissions:
contents: write
pull-requests: write

concurrency:
group: camera-data-sync
cancel-in-progress: false

jobs:
sync:
runs-on: ubuntu-latest

steps:
- name: Checkout develop
uses: actions/checkout@v5
with:
ref: develop
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'

- name: Validate sync script
run: node --check scripts/sync-cameras.mjs

- name: Sync source coordinates and VietMap addresses
env:
VIETMAP_API_KEY: ${{ secrets.VIETMAP_API_KEY }}
HCM_TRAFFIC_COOKIE: ${{ secrets.HCM_TRAFFIC_COOKIE }}
run: |
args=()
if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.force_geocode }}" == "true" ]]; then
args+=(--force-geocode)
fi
node scripts/sync-cameras.mjs "${args[@]}"

- name: Detect data changes
id: changes
run: |
if git diff --quiet -- data-camera.json; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "No camera data changes."
else
echo "changed=true" >> "$GITHUB_OUTPUT"
git diff --stat -- data-camera.json
fi

- name: Create or update sync pull request
if: steps.changes.outputs.changed == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
BRANCH="data/auto-sync-cameras"

git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git fetch origin "$BRANCH:refs/remotes/origin/$BRANCH" || true
git checkout -B "$BRANCH"
git add data-camera.json
git commit -m "data: sync camera locations"
git push --force-with-lease origin "HEAD:$BRANCH"

existing_pr="$(gh pr list --repo "$GITHUB_REPOSITORY" --base develop --head "$BRANCH" --state open --json url --jq '.[0].url // ""')"
if [[ -n "$existing_pr" ]]; then
echo "Updated existing PR: $existing_pr"
else
gh pr create \
--repo "$GITHUB_REPOSITORY" \
--base develop \
--head "$BRANCH" \
--title "data: sync traffic camera locations" \
--body "Automated camera data sync from the HCMC traffic source. Adds new published cameras, refreshes source coordinates, and fills missing address fields through VietMap when VIETMAP_API_KEY is configured. Existing source-missing cameras are retained for manual review."
fi
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,35 @@ The project exposes crawlable resources in addition to the JavaScript camera dir
- `manifest.webmanifest` and `favicon.svg` — app/site identity metadata.

The homepage and detail pages include canonical URLs, social metadata and Schema.org JSON-LD.

## Đồng bộ tọa độ & địa chỉ

`data-camera.json` lưu tọa độ gốc từ `Location.Shape` của nguồn camera TP.HCM:

```json
{
"Latitude": 10.7918902432446,
"Longitude": 106.691054105759
}
```

`scripts/sync-cameras.mjs` thực hiện:

1. Gọi `SearchQuery` với `Publish:true`.
2. Parse AjaxPro `DataTable` và lấy `POINT(longitude latitude)`.
3. Thêm camera mới và cập nhật tọa độ nguồn.
4. Giữ lại camera cũ không còn xuất hiện ở nguồn để review thủ công.
5. Nếu có `VIETMAP_API_KEY`, gọi VietMap Reverse v4 (`display_type=6`) để bổ sung `Address`, `Ward`, `District` và `Province`.
6. Chỉ reverse-geocode camera chưa có địa chỉ hoặc camera có tọa độ thay đổi.

GitHub Actions chạy mỗi 6 giờ và chỉ tạo/update PR `data/auto-sync-cameras` khi `data-camera.json` thực sự thay đổi.

### Repository secrets

Vào **Settings → Secrets and variables → Actions** và thêm:

- `VIETMAP_API_KEY` — API key VietMap. Không được commit key vào repository.
- `HCM_TRAFFIC_COOKIE` — tùy chọn; chỉ cần nếu endpoint nguồn bắt buộc session/cookie khi chạy từ GitHub Actions.

Có thể chạy tay workflow **Sync camera data** và bật `force_geocode` khi muốn reverse-geocode lại toàn bộ camera có tọa độ.

38 changes: 33 additions & 5 deletions camera.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,24 @@ function cameraDistrict(array $camera): string
$canonical = BASE_URL . '/';
$cameraName = 'Không tìm thấy camera';
$district = 'TP.HCM';
$address = '';
$ward = '';
$province = 'Hồ Chí Minh';
$latitude = null;
$longitude = null;
$imageUrl = '';
$related = [];
} else {
$cameraName = trim((string) $camera['CamName']);
$district = cameraDistrict($camera);
$address = trim((string) ($camera['Address'] ?? ''));
$ward = trim((string) ($camera['Ward'] ?? ''));
$province = trim((string) ($camera['Province'] ?? 'Hồ Chí Minh'));
$latitude = isset($camera['Latitude']) && is_numeric($camera['Latitude']) ? (float) $camera['Latitude'] : null;
$longitude = isset($camera['Longitude']) && is_numeric($camera['Longitude']) ? (float) $camera['Longitude'] : null;
$locationText = $address !== '' ? $address : $district . ', TP.HCM';
$pageTitle = 'Camera ' . $cameraName . ' – ' . $district . ' | TP.HCM';
$pageDescription = 'Xem camera giao thông ' . $cameraName . ' tại ' . $district . ', TP.HCM. Hình ảnh camera được cập nhật tự động để tiện theo dõi tình hình giao thông.';
$pageDescription = 'Xem camera giao thông ' . $cameraName . ' tại ' . $locationText . '. Hình ảnh camera được cập nhật tự động để tiện theo dõi tình hình giao thông.';
$canonical = BASE_URL . '/camera.php?id=' . rawurlencode($cameraId);
$imageUrl = BASE_URL . '/proxy.php?id=' . rawurlencode($cameraId);

Expand Down Expand Up @@ -84,12 +95,20 @@ function cameraDistrict(array $camera): string
'about' => [
'@type' => 'Place',
'name' => $cameraName,
'address' => [
'address' => array_filter([
'@type' => 'PostalAddress',
'addressLocality' => 'Hồ Chí Minh',
'addressRegion' => $district,
'streetAddress' => $address,
'addressLocality' => $ward !== '' ? $ward : $district,
'addressRegion' => $province,
'addressCountry' => 'VN',
],
], static fn ($value): bool => $value !== ''),
...($latitude !== null && $longitude !== null ? [
'geo' => [
'@type' => 'GeoCoordinates',
'latitude' => $latitude,
'longitude' => $longitude,
],
] : []),
],
'primaryImageOfPage' => [
'@type' => 'ImageObject',
Expand Down Expand Up @@ -224,6 +243,12 @@ function cameraDistrict(array $camera): string
.content { padding: clamp(18px, 4vw, 30px); }
.eyebrow { color: var(--primary); font-size: .78rem; font-weight: 800; text-transform: uppercase; letter-spacing: .05em; }
h1 { margin: 7px 0 10px; font-size: clamp(1.45rem, 4vw, 2.15rem); line-height: 1.25; }
.address-line {
margin: -4px 0 18px;
color: var(--muted);
line-height: 1.6;
}
.address-line strong { color: var(--text); }
.lead { margin: 0; color: var(--muted); line-height: 1.7; }
.meta {
display: flex;
Expand Down Expand Up @@ -297,6 +322,9 @@ function cameraDistrict(array $camera): string
<div class="eyebrow">Camera giao thông TP.HCM</div>
<h1><?= h($cameraName) ?></h1>
<p class="lead"><?= h($pageDescription) ?></p>
<?php if ($address !== ''): ?>
<p class="address-line"><strong>Địa chỉ:</strong> <?= h($address) ?></p>
<?php endif; ?>
<div class="meta">
<span class="pill"><?= h($district) ?></span>
<span class="pill">Mã camera: <?= h(strtoupper(substr($cameraId, -6))) ?></span>
Expand Down
Loading
Loading