
A veces hay días así.
'Uh... creo que pasé rápido frente a una cámara de control de velocidad hace un rato.'
Sin embargo, no pude encontrar fácilmente la ubicación de la cámara de control de velocidad.
Así que decidí intentar hacerlo.
1. Datos públicos de Gyeonggi

Dado que suelo ir a la provincia de Gyeonggi, pensé en usar los datos públicos de Gyeonggi.
Al entrar, encontré un ejemplo ya implementado con la API de Kakao Map.
Parecía que esto era lo que quería hacer.

Al revisar los datos, vi que las cámaras de control no solo capturan exceso de velocidad sino también otras infracciones.
Tipos: 01: velocidad, 02: señal, 03: violación de tránsito, 04: estacionamiento ilegal, 99: otros, en total 5 tipos.
Ahora, dado que es TypeScript, tengo que especificar el tipo de retorno...

¿En total hay 24 elementos? Es un montón.
Descargué un archivo json y lo cargué en Ruiten para imprimir el tipo.
AI es realmente útil.

interface cameraDataType {
MNLSS_TEFCM_INFO: string;
SIDO_NM: string;
SIGUN_NM: string;
ROAD_KIND_NM: string;
ROUTE_MANAGE_NO_INFO: string | null; // Se permite null por valores vacíos
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;
}Voy a especificar así el tipo y leer el archivo json para mostrarlo en el mapa.
Como las cámaras de control no aparecen de repente, solo para probar está bien.
2. API de Kakao Map
Ahora es el momento de hacer las configuraciones básicas para usar la API de Kakao Map.
Entra en el Kakao Developers.

Entra en mi aplicación.
Si no tienes una aplicación, crea una.

Y registra la plataforma.
Por defecto, el uso de la API está restringido a los dominios del sitio registrados aquí.

Luego activa la configuración en Kakao Map en la parte inferior.

Por último, verifica tu clave API.

Ahora copia esta clave en mi .env.
Como es una clave que se expondrá, se debe configurar como nextpublic, pero no importa ponerla directamente en JavaScript.
3. Implementación del mapa usando el SDK de la API de Kakao Map
Quería hacerlo rápidamente, así que decidí probar una biblioteca de Kakao Map que encontré en GitHub.
Enlazo al GitHub de JaeSeoKim.
Primero, instalo la biblioteca.
yarn add react-kakao-maps-sdkDicha biblioteca ofrece una variedad de tipos.
Configura tsconfig.json de la siguiente manera.
{
...,
"compilerOptions": {
...,
"types": [
...,
"kakao.maps.d.ts"
]
}
}Si typeRoots está presente, esto no funcionará.
En ese caso, configúralo así.
{
"compilerOptions": {
...
"typeRoots": [
"./node_modules/kakao.maps.d.ts/@types"
]
...
}
}Ahora que hemos terminado la configuración básica, hagamos un componente para invocar Kakao Map.
Dará un error si se llama al mapa antes de que se carguen todos los scripts.
Así que hacemos que la visualización del mapa se realice después de comprobar el estado del script con 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 // Contenedor para mostrar el mapa
style={{ height: "calc(100svh - 100px)" }}
className="w-full"
id="map"
center={{ lat: 37.44749167, lng: 127.1477194 }}
level={3} // Nivel de zoom del mapa
>
<MapTypeControl position={"TOPRIGHT"} />
<ZoomControl position={"RIGHT"} />
</Map>
) : (
<div></div>
)}
</div>
);
}Ahora se puede invocar esto desde una página o colocarlo directamente en la página para que se ejecute como se muestra a continuación.
Ya que el mapa ha aparecido, se podría decir que es medio éxito.

4. Crear marcadores
Ahora es el momento de extraer la latitud, longitud y datos de límite de velocidad de los datos de la cámara y mostrarlos en el mapa.
El método para mostrar marcadores en el SDK es bastante simple.
Solo introduce la latitud y longitud del lugar donde deseas mostrarlo y listo.
<MapMarker position={{ lat: 37, lng: 128 }} />Al ingresar una latitud y longitud al azar, el pin se colocó en una montaña en Chungju.

Y al colocar un ReactNode dentro de la etiqueta, también muestra texto.
<MapMarker position={{ lat: 37, lng: 128 }}>
<div>Uh.. ¿Está aquí?</div>
<div>¿Es esta la latitud 37 grados?</div>
</MapMarker>
En realidad, no es muy bonito.
Por lo tanto, también se admite la superposición personalizada.
Usa customOverlayMap y luego úsalo como se indica a continuación.
<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>Uh.. ¿Está aquí?</div>
<div>¿Es esta la latitud 37 grados?</div>
</div>
</div>
</CustomOverlayMap>
Lo más importante es que debe estar fuera de la etiqueta MapMarker.
Si la declaras dentro de MapMarker, permanecerá una caja blanca.
Ahora hagamos una función para esto.
Creé un tipo llamado cameraCustomOverlay que coincide con los datos que necesito que entren en la función.
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} // Posición del marcador
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">Información de la cámara</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>Velocidad límite : {data.limit}</div>
</div>
</div>
</div>
</CustomOverlayMap>
) : null}
</>
);
};Cuando intenté usar el evento de hover, no funcionó en el móvil.
Así que utilicé useState para poder abrir y cerrar la superposición.
Luego, simplemente procesa el json y añade las etiquetas con map.
5. Cargar datos
Lee y analiza el json, elimina los controles de estacionamiento con un filtro.
Luego, pasa la información necesaria como latitud, longitud, lugar de instalación y límite de velocidad al componente correspondiente.
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>
);
}Ahora, en el componente, utiliza esos datos para mostrar los marcadores.
export default function KakaoMapPage({
data,
}: {
data: cameraCustomOverlay[];
}) {
...
{scriptLoad ? (
<Map // Contenedor para mostrar el mapa
style={{ height: "calc(100svh - 100px)" }}
className="w-full"
id="map"
center={{lat: 37.44749167,lng: 127.1477194}}
level={3} // Nivel de zoom del mapa
>
{data.map((camera, idx) => (
<EventMarkerContainer // Crear marcador
key={idx}
data={camera}
/>
))}
<MapTypeControl position={"TOPRIGHT"} />
<ZoomControl position={"RIGHT"} />
</Map>
) : (
<div></div>
...
);
}De esta manera, los marcadores se mostrarán y las superposiciones personalizadas aparecerán al hacer clic en ellos.
Al probarlo, funciona bien.

6. Obtener información de ubicación del usuario
El uso de navigator permite obtener la información de GPS del usuario.
Utiliza useState para configurar la ubicación por defecto y, en useEffect, usar navigator para obtener información de ubicación del usuario y reasignar la ubicación del mapa.
...
const [mapCenter, setMapCenter] = useState({
lat: 37.44749167,
lng: 127.1477194, // Ubicación predeterminada en Seongnam, Gyeonggi
});
useEffect(() => {
const { geolocation } = navigator;
geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords;
setMapCenter({
lat: latitude,
lng: longitude, // Reasignar según la ubicación del usuario
});
},
(err) => console.log(err),
{ enableHighAccuracy: true, timeout: 5000, maximumAge: 0 }
);
}, []);
...7. Problemas
El mapa se volvió demasiado lento debido a la gran cantidad de datos de CCTV.
Vamos a resolver este problema mediante el uso de clústeres.
<MarkerClusterer averageCenter={true} minLevel={6}>
{data.map((camera, idx) => (
<EventMarkerContainer // Crear marcador
key={idx}
data={camera}
/>
))}
</MarkerClusterer>En el SDK, simplemente envolverlo con la etiqueta MarkerClusterer es suficiente.
Sin embargo, el problema es que la etiqueta CustomOverlayMap también se reconoce como una etiqueta de marcador, y la respuesta al clic cambia un poco.
Si deseas usar marcadores y superposiciones personalizadas junto con el clúster, debes separar las etiquetas como se muestra a continuación.
<MarkerClusterer averageCenter={true} minLevel={6}>
{data.map((camera, idx) => (
<MapMarker // Crear marcador
key={idx}
position={{ lat: camera.lat, lng: camera.lng }}
/>
))}
</MarkerClusterer>;
{
data.map((camera, idx) => (
<CustomOverlayMap key={idx} position={{ lat: camera.lat, lng: camera.lng }}>
Contenido deseado
</CustomOverlayMap>
));
}Ahora, debemos hacer que cada MapMarker y CustomOverlayMap compartan el useState.
Esto se puede lograr utilizando la clave.
// Primero, utiliza useState para especificar la clave del marcador activado.
const [activeMarkerId, setActiveMarkerId] = useState<null|number>(null);
...
<MarkerClusterer averageCenter={true} minLevel={6}>
{data.map((camera, idx) => (
<MapMarker // Crear marcador
key={idx}
position={{ lat: camera.lat, lng: camera.lng }}
onClick={()=> {
setActiveMarkerId(activeMarkerId === idx ? null : idx);
// Al hacer clic en un marcador, especificar el índice del marcador como clave de activación.
// Si la clave actual es igual a la clave del marcador, desactivarla.
}}
/>
))}
</MarkerClusterer>
{data.map((camera, idx) => (
// Solo se muestra la superposición personalizada si coincide con la clave activada.
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">Información de la cámara</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>Velocidad límite : {camera.limit}</div>
</div>
</div>
</div>
</CustomOverlayMap>
)
))}Como profesor de geociencias, creo que he profundizado demasiado en esto.
Ahora hagamos una última prueba.

8. Reseña
He estado experimentando con el SDK de la API de Kakao Map durante dos días.
Fue frustrante encontrar lo que no funcionaba, pero al final sentir la satisfacción de resolverlo era genial.
Si tuviera tiempo, también me gustaría probar con la propia API, pero parece que no tengo suficiente tiempo.
Creo que esto es todo para mí...
Aún así, fue un momento agradable aprender algo nuevo.
댓글을 불러오는 중...