使用地政處 CSDI 和CesiumJS製作三維廁所地圖

最近摸緊Cesium同埋工作關係都有機會接觸CSDI
為左熟習Cesium有咩功能所以就有呢個exercise

我會點形容Cesium呢,就係一個open platform 專係做宏觀既3D viewer
唔同Autodesk forge 做唔到一d open world既demo
所以(應該)好適合做city planning

而CSDI 就係LandsD 既一個database 收集左好多唔同部門provide既五花八門既asset data
例如廁所,停車場資料,學校,仲有好多意想不到既野
但佢有樣野唔好,就係data 既attribute format 好明顯冇夾過就草草咁入資料落去
對programmer 唔friendly

入返正題先,首先拎左香港既公廁資料先, 入去CSDI個網 csdi.png search toilet 但搵唔到有用既dataset
公廁應該歸食環境署管既,search by department
fehd.png toilet data.png 仲唔搵到你

click download,要csv format,貪佢夠哂簡單, 正式用cesium 之前做多野樣,就係轉佢做json format
寫code直接read csv 最parse做json都得但唔係今次重點,
所以最後用左online csv to json converter

之後就黎到Cesium果part
如果只係做lab心態既話cesium 其實有個default api key已經做到好多野
同以往一樣,用nextjs, cesium 有cesium 既library, 另外就有隻叫resium既react support

npx create-next-app@latest
npm install --save cesium resium

裝完之後就可以寫code
寫左個底食左個json file 先

 const file = await fs.readFile(process.cwd() + '/app/toilet.json', 'utf8');
 const data = JSON.parse(file);

load resium library

const Cesium = dynamic(
  () => import('../components/Cesium'),
  { ssr: false }
)

就係咁簡單

import dynamic from 'next/dynamic'
import { promises as fs } from 'fs';

const Cesium = dynamic(
  () => import('../components/Cesium'),
  { ssr: false }
)

export default async function Home() {

  const file = await fs.readFile(process.cwd() + '/app/toilet.json', 'utf8');
  const data = JSON.parse(file);

  return (
    <div className="h-screen w-screen">
      <Cesium data={data} />
    </div>
  )
}

之後寫Cesium呢個custom components
其實就係直接call resium入面既components

        <Viewer
          style={{ height: "100%" }}
          terrainProvider={terrainProvider}
          ref={(e) => {
            viewer = (e && e.cesiumElement)!;
          }}
        >
          <Cesium3DTileset url={IonResource.fromAssetId(96188)} />
        </Viewer>

裡面有行

<Cesium3DTileset url={IonResource.fromAssetId(96188)} />

其實就係直接係Cesium Ion 既library度去拎個asset id = 96188 既model 出黎
96188 就係cesium 自己既3D building tiles

run黎試下就會有呢個畫面 Screenshot 2024-01-23 110500.png

之後處理返csdi既數據
睇返佢提供既data spec
csdi spec.png 可以用SEARCH02_EN黎做filtering

  const toiletData = data
    .filter((item) => item["SEARCH02_EN"] === "PUBLIC TOILETS")
    .sort((a, b) =>
      (a["SEARCH01_TC"] as string).localeCompare(b["SEARCH01_TC"] as string)
    );

另外仲有lat 同long既數值 咁就可以create 返toilet既marker

          {toiletData.map((item, index) => (
            <Entity
              key={index}
              onClick={() => {
                const point = Cartesian3.fromDegrees(
                  parseFloat(item["LONGITUDE"]),
                  parseFloat(item["LATITUDE"]),
                  100
                );
                viewer.camera.flyTo({ destination: point });
              }}
              name={item["NAME_TC"]}
              point={{ pixelSize: 20 }}
              description={item["ADDRESS_TC"]}
              position={Cartesian3.fromDegrees(
                parseFloat(item["LONGITUDE"]),
                parseFloat(item["LATITUDE"]),
                100
              )}
            />
          ))}

再加多少少wrap up
成品如下
toilet+viewer.gif

Copyright © tnlo.me. All rights reserved.