
時々、そんな日があります。
「あれ...さっきスピード監視カメラの前を速く通り過ぎたような気がするけど?」
しかし、スピード監視カメラの位置を見ようとしてもなかなか見つかりませんでした。
それで一度作ってみることにしました。
1. 京畿公データ

私が主に行く場所が京畿道だったので、京畿公データを使ってみることにしました。
見てみると既にカカオマップAPIで実装された例がありました。
作るとこんな感じになるかなと思いました。

とりあえずデータを見ると監視カメラは単純なスピード監視だけでなく、いくつかありました。
種類は 01:速度、02:信号、03:通行違反、04:不法駐車、99:その他、計5種類。
これからはTypeScriptなので返却タイプを指定する必要がありますが...

出力値が合計24個あるね?無駄に多いです。
とりあえずjsonデータを一つダウンロードしてRutenでタイプを出力しました。
やっぱり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 Map API
今度はカカオマップAPIを利用するための基本的な設定をする番です。
カカオディベロッパーに入ります。

そして自分のアプリケーションに入ります。
アプリケーションがない場合は一つ作りましょう。

そしてプラットフォームを登録します。
基本的にここに登録されたサイトドメイン以外ではAPIの使用が制限されます。

そして下のカカオマップに入ってアクティベーション設定をします。

最後に自分のAPIキーを確認します。

これでこのキーをコピーして自分の.envに入れておけばいいです。
どうせ公開される、nextpublicに設定する必要があるキーなので単にJavaScriptに放り込んでも構いません。
3. Kakao Map API SDKを利用した地図の実装
早く作ってみたくてgithubで見つけたカカオマップライブラリを使ってみることにしました。
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"
]
...
}
}基本的な準備が終わったのでカカオマップを呼び出すためのコンポーネントを作ってみましょう。
スクリプトが全てロードされる前に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>
正直、あまり美しくないです。
それでカスタムオーバーレイもサポートしています。
カスタムオーバーレイはcustumOverlayMapを呼び出した後に使うことができます。
<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を読み取りパースした後、駐車監視はフィルターで除去します。
そして私に必要な情報である経緯度、設置場所、速度制限をコンポーネントに渡しました。
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. 問題点
CCTVデータが多すぎて地図がすごく重くなりました。
今度はこの問題をクラスタで解決してみましょう。
<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を共有できるようにしてあげる必要があります。
これはキーを利用すれば良いです。
// まずuseStateでアクティブにするマーカーのキーを指定します。
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をアクティブキーに指定。
// もし既存のキーがマーカーのキーと同じ場合はキーを解除。
}}
/>
))}
</MarkerClusterer>
{data.map((camera, idx) => (
// アクティブなキーと一致する場合だけカスタムオーバーレイを表示します。
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. レビュー
2日間カカオマップAPI SDKに挑戦してみました。
できないことを見つけるのに悩みましたが、結局解決したので嬉しいです。
時間があればAPI自体も触ってみたいですが、その時間はなさそうです。
私にはこの程度が限界かも...
それでも新しいことを学べて楽しい時間でした。
댓글을 불러오는 중...