
有时候会有这样的日子。
'哦... 刚才好像在超速摄像头前快速经过了?'
但是想查看超速摄像头的位置也不容易找到。
所以决定自己做一个。
1. 京畿公共数据

由于我主要去的地方是京畿道,所以决定使用京畿公共数据。
进去一看,发现已经有用 Kakao 地图 API 实现的例子了。
做出来应该是这样的感觉。

从数据来看,摄像头不仅只是超速,还有其他许多种类。
种类包括 01:速度,02:信号,03:通行违规,04:非法停车,99:其他,总共 5 类。
现在因为是 TypeScript,所以需要指定返回类型...

输出值总共有 24 个?未免太多了。
先下载一个 json 数据,然后在 Ruitn 中丢进去,输出类型。
还是 AI 方便。

interface cameraDataType {
MNLSS_TEFCM_INFO: string;
SIDO_NM: string;
SIGUN_NM: string;
ROAD_KIND_NM: string;
ROUTE_MANAGE_NO_INFO: string | null; // 可能为空,因此允许为 null
ROUTE_NM: string;
ROUTE_INFO: string;
REFINE_ROADNM_ADDR: string;
REFINE_LOTNO_ADDR: string;
REFINE_WGS84_LAT: string;
REFINE_WGS84_LOGT: string;
INSTL_PLC: string;
REGLT_DIV_NM: string;
LIMITN_SPEED_INFO: string;
PRTCAREA_DIV: string;
INSTL_YY: string;
MNGINST_NM: string;
MNGINST_TELNO: string;
DATA_STD_DE: string;
}指定类型后,读入 JSON 文件并在地图上显示。
因为摄像头不会突然大量增加,所以这样做没问题。
2. Kakao 地图 API
现在该为使用 Kakao 地图 API 做基本设置了。
进入Kakao 开发者。

然后进入我的应用程序。
如果没有自己的应用程序,就创建一个。

注册平台。
基本上除注册在这里的站点域名外,API 使用受限。

然后进入下方的 Kakao 地图,启用设置。

最后确认我的 API key。

将这个 key 复制到我的 .env 中。
因为是要暴露的 nextpublic key,直接绑定在 JavaScript 中也无所谓。
3. 使用 Kakao Map API SDK 实现地图
迫不及待想要制作了,于是决定使用在 GitHub 上找到的 Kakao 地图库。
链接JaeSeoKim 的 GitHub。
首先安装库。
yarn add react-kakao-maps-sdk该库支持多种类型。
在 tsconfig.json 中按如下设置。
{
...,
"compilerOptions": {
...,
"types": [
...,
"kakao.maps.d.ts"
]
}
}如果有 typeRoots,这个就不起作用。
这时按如下设置即可。
{
"compilerOptions": {
...
"typeRoots": [
"./node_modules/kakao.maps.d.ts/@types"
]
...
}
}基本准备完成后,我们创建调用 Kakao 地图的组件。
脚本加载完成前调用 Map 会出错。
因此,通过 onReady 进行检查,加载完成后显示地图。
"use client";
import { Map, MapTypeControl, ZoomControl } from "react-kakao-maps-sdk";
import { useEffect, useState } from "react";
import { cameraCustomOverlay } from "@/types/allTypes";
import Script from "next/script";
export default function KakaoMapPage({
data,
}: {
data: cameraCustomOverlay[];
}) {
const [scriptLoad, setScriptLoad] = useState<boolean>(false);
return (
<div className="items-center">
<Script
src={`//dapi.kakao.com/v2/maps/sdk.js?appkey=${process.env.NEXT_PUBLIC_KAKAO_API_KEY}&autoload=false&libraries=clusterer`}
onReady = {() => setScriptLoad(true)}
/>
{scriptLoad ? (
<Map // 显示地图的容器
style={{ height: "calc(100svh - 100px)" }}
className="w-full"
id="map"
center={{ lat: 37.44749167, lng: 127.1477194 }}
level={3} // 地图缩放级别
>
<MapTypeControl position={"TOPRIGHT"} />
<ZoomControl position={"RIGHT"} />
</Map>
) : (
<div></div>
)}
</div>
);
}现在可以在 Page 中调用这个,或把它插入 Page,运行结果如下。
已经能显示地图,一半成功了。

4. 制作标记
现在从摄像头数据中提取经度、纬度和速度限制数据,进行地图显示。
SDK 中标记的显示方法相当简单。
只需填入想显示位置的经度和纬度就完事。
<MapMarker position={{ lat: 37, lng: 128 }} />粗略填入纬度和经度,结果标在忠州一处山野上。

在标签中加入 ReactNode,还能显示文字。
<MapMarker position={{ lat: 37, lng: 128 }}>
<div>哦.. 这里是</div>
<div>纬度 37 度吗?</div>
</MapMarker>
其实不算漂亮。
所以也支持自定义覆盖物。
使用 customOverlayMap 后即可使用。
<MapMarker position={{ lat: 37, lng: 128 }}></MapMarker>
<CustomOverlayMap position={{ lat: 37, lng: 128 }} yAnchor={2}>
<div>
<div className="rounded-lg p-2 border border-red-500 bg-violet-600 text-white">
<div>哦.. 这里是</div>
<div>纬度 37 度吗?</div>
</div>
</div>
</CustomOverlayMap>
关键是要在 MapMarker 标签外写。
在 MapMarker 内部声明会始终留着白框。
现在将其变为函数。
根据函数中数据的需要制作 cameraCustomOverlay 类型。
interface cameraCustomOverlay {
lat: number;
lng: number;
title: string;
limit: number;
}
const EventMarkerContainer = ({ data }: { data: cameraCustomOverlay }) => {
const [isVisible, setIsVisible] = useState(false);
const position = { lat: data.lat, lng: data.lng };
return (
<>
<MapMarker
position={position} // 显示标记的位置
onClick={() => setIsVisible(!isVisible)}
/>
{isVisible ? (
<CustomOverlayMap
position={position}
yAnchor={1.5}
key={`overlay-${data.lat}-${data.lng}`}
>
<div>
<div className="bg-gray-50 text-black border-gray-500 rounded-lg border-2 relative">
<div className="bg-slate-500 text-white p-2">摄像头信息</div>
<div className="absolute top-0 right-0 p-2">
<XCircleIcon
className="size-6 text-white"
onClick={() => setIsVisible(!isVisible)}
/>
</div>
<div className="p-2">
<div className="break-words">{data.title}</div>
<div>限速 : {data.limit}</div>
</div>
</div>
</div>
</CustomOverlayMap>
) : null}
</>
);
};将鼠标悬停事件改变为点击事件,因为在手机上不能运行。
用 useState 使覆盖物可开关。
加工 JSON 后用 map 将标签插入即可。
5. 数据加载
读取 JSON,并解析后,用 filter 移除停车违规。
然后将经纬度、安装位置、限速等必要信息传递给组件。
import KakaoMapPage from "@/components/KakaoMapPage";
import fs from "fs";
import { cameraJsonPath } from "@/data/filePaths";
import { cameraDataType } from "@/types/allTypes";
export default function Page() {
const dataJson = fs.readFileSync(cameraJsonPath, "utf-8");
const allData: cameraDataType[] = JSON.parse(dataJson);
const filterdMarkerData = allData.filter(
(data) => !data.REGLT_DIV_NM.includes("4")
);
const markerData = filterdMarkerData.map((data) => {
return {
lat: Number(data.REFINE_WGS84_LAT),
lng: Number(data.REFINE_WGS84_LOGT),
title: data.INSTL_PLC,
limit: Number(data.LIMITN_SPEED_INFO),
};
});
return (
<div className="items-center">
<KakaoMapPage data={markerData} />
</div>
);
}现在组件中利用这些数据显示标记。
export default function KakaoMapPage({
data,
}: {
data: cameraCustomOverlay[];
}) {
...
{scriptLoad ? (
<Map // 显示地图的容器
style={{ height: "calc(100svh - 100px)" }}
className="w-full"
id="map"
center={{lat: 37.44749167,lng: 127.1477194}}
level={3} // 地图缩放级别
>
{data.map((camera, idx) => (
<EventMarkerContainer // 创建标记
key={idx}
data={camera}
/>
))}
<MapTypeControl position={"TOPRIGHT"} />
<ZoomControl position={"RIGHT"} />
</Map>
) : (
<div></div>
...
);
}这样就可以显示标记,并在点击时显示自定义覆盖物。
一试,能很好地运行。

6. 获取用户位置信息
利用 navigator 可以获取用户的 GPS 信息。
用 useState 指定基本位置,在 useEffect 中用 navigator 获取用户位置信息,并重新分配地图位置。
...
const [mapCenter, setMapCenter] = useState({
lat: 37.44749167,
lng: 127.1477194, // 基本位置 京畿道 城南
});
useEffect(() => {
const { geolocation } = navigator;
geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords;
setMapCenter({
lat: latitude,
lng: longitude, // 重新分配为用户位置
});
},
(err) => console.log(err),
{ enableHighAccuracy: true, timeout: 5000, maximumAge: 0 }
);
}, []);
...7. 问题
由于摄像头数据太多,地图非常卡顿。
现在用聚类来解决这个问题。
<MarkerClusterer averageCenter={true} minLevel={6}>
{data.map((camera, idx) => (
<EventMarkerContainer // 创建标记
key={idx}
data={camera}
/>
))}
</MarkerClusterer>在 SDK 中,只需用 MarkerClusterer 标签包裹即可。
但问题在于 CustomOverlayMap 标签也被识别为标记,响应略有不同。
如果同时使用聚类和自定义覆盖物,需要像下面这样分离标签。
<MarkerClusterer averageCenter={true} minLevel={6}>
{data.map((camera, idx) => (
<MapMarker // 创建标记
key={idx}
position={{ lat: camera.lat, lng: camera.lng }}
/>
))}
</MarkerClusterer>;
{
data.map((camera, idx) => (
<CustomOverlayMap key={idx} position={{ lat: camera.lat, lng: camera.lng }}>
我想要的内容
</CustomOverlayMap>
));
}现在要让各自的 MapMarker 和 CustomOverlayMap 共享 useState。
可以利用 key 实现。
// 先用 useState 指定激活标记的 key。
const [activeMarkerId, setActiveMarkerId] = useState<null|number>(null);
...
<MarkerClusterer averageCenter={true} minLevel={6}>
{data.map((camera, idx) => (
<MapMarker // 创建标记
key={idx}
position={{ lat: camera.lat, lng: camera.lng }}
onClick={()=> {
setActiveMarkerId(activeMarkerId === idx ? null : idx);
// 点击标记时,将标记的 idx 设置为激活 key。
// 如果现有 key 与标记的 key 相同,则解除绑定。
}}
/>
))}
</MarkerClusterer>
{data.map((camera, idx) => (
// 仅当与激活的 key 一致时才显示覆盖物。
activeMarkerId === idx && (
<CustomOverlayMap
key={idx}
yAnchor={1.5}
position={{ lat: camera.lat, lng: camera.lng }}
>
<div>
<div className="bg-gray-50 text-black border-gray-500 rounded-lg border-2 relative">
<div className="bg-slate-500 text-white p-2">摄像头信息</div>
<div className="absolute top-0 right-0 p-2">
<XCircleIcon className="size-6 text-white"
onClick={() => setActiveMarkerId(null)}
/>
</div>
<div className="p-2">
<div className="break-words">{camera.title}</div>
<div>限速 : {camera.limit}</div>
</div>
</div>
</div>
</CustomOverlayMap>
)
))}对于地理科学老师来说,这样已经走得太深了。
最后再试一次。

8. 后记
用了两天时间研究 Kakao 地图 API SDK。
找到不工作原因时很伤脑筋,但最后解决时感觉很棒。
如果有时间要试试 API 本身,但可能没有时间。
我大概就只能到这儿了...
不过,能学到新东西,度过了愉快的时光。
댓글을 불러오는 중...