-
leaflet-rotate로 leaflet 지도의 각도를 변경해보자.기록이라도 하자 2023. 8. 14. 13:44
- leaflet에서 지도의 각도를 변경할 수 있는 자체 속성은 없다.
- 그럼에도 불구하고 지도의 각도를 변경하게 해달라는 요청이 고객들로부터 지속적으로 들어왔다.
현장에서 작업을 할 때 작업자들이 현장을 바라보는 특정한 각도가 있는데, 서비스에서 제공하는 지도에서는 각도가 항상 고정되어 있었기 때문에 불편했던 것이다.
더군다나 도면에 써있는 숫자나 글씨까지도 원래 사용하는 각도로 만들어져 있기 때문에 도면이 뒤집어진 채로 사용해야 했다.
면적이 극도로 큰 현장에서 더 크게 불편함을 느꼈고, 기술 지원 팀에게 매일 전화 요청이 들어오면서 본격적으로 작업에 착수했다.
leaflet-rotate를 사용하여 지도의 각도를 변경했고, 이 라이브러리는 데모는 잘 만들어진 것에 반해 사용 설명서(API 문서)가 없어서 멘땅에 헤딩을 많이 했다.
chatGPT와 함께 고군분투하며 문제를 해결해나갔던 과정을 기록해보았다.
1. 라이브러리 선택 - leaflet-rotate
https://github.com/Raruto/leaflet-rotate
- 라이브러리 선택 기준이라는 말이 무색하게 leaflet rotate라는 키워드로 검색했을 때 결과로 나오는 라이브러리가 이것 외에 거의 없었다. 이건 그나마 weekly downloads수가 많아서 사용하기 안심이 되었다.
- 하지만 react용 라이브러리는 아니라 조금 걱정했지만 다른 대안은 없으니 무작정 임포트하면 된다.
- leaflet 지도가 있는 파일에 import 'leaflet-rotate' 를 입력하면 끝난다.
- https://raruto.github.io/leaflet-rotate/examples/leaflet-rotate.html
2. leaflet map에 속성 추가
import { Map } from 'react-leaflet'; import 'leaflet-rotate'; ... export default function TwodMap({}){ return ( <Map rotate={true} touchRotate={true} bearing={90} rotateControl={{ closeOnZeroBearing: false, position: 'topleft', }} {...} /> )
rotate는 지도의 각도를 변경할 수 있게 하는 속성이고, touchRotate는 터치 이벤트로 각도를 변경할 수 있는지 여부를 나타내는 속성이다. bearing에는 변경할 각도를 입력하면 된다.
rotateControl에 값을 설정하면 각도를 설정할 수 있는 컨트롤이 지도위에 표시되는데 position은 지도 어느쪽에 표시할지를 정하는 값이고, closeOnZeroBearing은 기본값이 true인데, 각도가 0이되면 컨트롤을 보이지 않게 하므로, false로 지정해야 항상 표시된다.3. 지도도구 렌더러 변경
이 상태로 각도를 변경하거나 줌레벨을 변경하면 Marker는 지도 위에 붙어서 각도가 함께 잘 변경되는데, Polyline이나 Polygon은 다른 패널에 붙어서 각도가 변경되지 않고 따로 놀기 시작한다.
tms로 타일링되어 있는 위성지도나, 도면, 정사사진등은 각도가 함께 잘 움직이는 것 같아서 Polyline과 Polygon의 renderer도 L.canvas()로 바꿔보았다. 그랬더니 Polyline과 Polygon도 각도가 잘 변경되었다.
하지만 개별 지도도구에 클릭이벤트가 동작하지 않았다. 전체 Path를 다 Canvas안에 그리다보니 선분 영역을 클릭했을 때 이벤트가 동작하지 않았던 것이다.
leaflet의 Path를 그릴때 사용하는 렌더러는 Svg와 Canvas 두가지가 있다. 모든 렌더러는 명시하지 않으면 암시적으로 동작(지도가 렌더러 유형을 결정하고 자동으로 사용)한다고 한다.
리플렛에서 제공하는 렌더러를 사용하는 것을 권장한다. L.svg(), L.canvas()가 있다.
그러나 테스트를 해보니 설정하지 않은 상태와 Svg를 설정한 상태, Canvas를 설정한 상태 세가지가 모두 지도에 표시된 이후의 동작이 달랐다. 그래서 설정하지 않은 상태는 무슨 값을 가지는지 여전히 의문이다..
- 설정하지 않은 상태 : Polyline, Polygon은 위치를 제대로 잡지 못한다. Marker는 각도가 변경되어도 지도에 붙어서 함께 잘 움직인다.
- Canvas: Polyline, Polygon도 위치를 잘 잡지만 각 레이어(지도도구)의 클릭 이벤트가 동작하지 않는다.
- Svg: Polyline, Polpygon의 위치도 잘 잡고 레이어의 클릭 이벤트도 동작한다.
<Polyline positions={positions} onClick={handleClick} bubblingMouseEvents={false} renderer={L.svg()} />
어쨌든 renderer에 L.svg()을 설정하면 클릭이벤트도 잘 동작하면서 각도가 변경된 상태로 표시된다. (여기서 L은 import L from 'leaflet';)
그리고 동적으로 마커를 지도위에 그려주는 경우가 있었는데 이 경우에도 렌더러를 설정해주지 않으면 각도가 변경되지 않는다. 그래서 처음엔 렌더러 설정할 생각을 못하고 현재 마우스 위치와, 지도의 중심 좌표와 각도를 가지고 변경된 위치를 계산하는 로직을 추가했다. 이 경우에도 렌더러를 변경해주면 간단하게 해결된다.
더보기사실 이 계산하는 것 때문에 이 작업이 조금 더 오래 걸렸는데, 나중에 이렇게 계산해도 지도를 조금 움직이거나 줌레벨을 변경할 경우 값이 튀는 현상이 발생했다. 그래서 생각해보니 마커에도 렌더러를 추가해주니 계산로직이 필요가 없었다.
혹시 필요한 분들을 위해 계산 로직을 첨부한다..
export default function rotateLatLng(map, latlng) { const centerLatLng = map.getCenter(); const angle = map.getBearing(); const centerPoint = map.latLngToLayerPoint(centerLatLng); const point = map.latLngToLayerPoint(latlng); // 회전 각도를 라디안 단위로 변환 const radian = (angle * Math.PI) / 180; // 중심 좌표를 기준으로 좌표 회전 const rotatedX = Math.cos(radian) * (point.x - centerPoint.x) - Math.sin(radian) * (point.y - centerPoint.y) + centerPoint.x; const rotatedY = Math.sin(radian) * (point.x - centerPoint.x) + Math.cos(radian) * (point.y - centerPoint.y) + centerPoint.y; // 회전된 좌표를 다시 지도 좌표로 변환 return map.layerPointToLatLng({ x: rotatedX, y: rotatedY }); }
<CircleMarker center={pointLatLng} radius={defaultRadius} renderer={L.svg()} />
4. 이벤트 전파 방지
지도도구(polygon, polyline) 안에 존재하는 툴팁을 클릭할 경우에도 이벤트가 전파되어 지도가 클릭되는 현상이 발생했다. 우리 서비스 같은 경우 지도도구 클릭된 상태에서는 지도도구 상세 화면이, 지도도구가 클릭되면 현장상황 상세화면으로 이동을 하는데, 툴팁을 클릭할 경우에 지도도구 클릭 동작과 마찬가지로 동작해야 했다.
이벤트가 전파되지 않도록 DomEvent.stopPropagation(e as any);을 써서 클릭이벤트 버블링을 막아주었다.
타입이 맞지 않아 에러가 발생하여 강제로 any로 타입 변환을 했다.
<CustomPolygon key={x.id} positions={x.positions.map(objectToLatLng)} onclick={(e) => { DomEvent.stopPropagation(e as any); onClick?.(x.id); }} > {!hiddenTooltip && x.name && <StyledTooltip key={x.id}>{x.name}</StyledTooltip>} </CustomPolygon>
5. 각도를 동적으로 변경
onRotate 함수를 사용해 값을 관리해야 한다.
우리 서비스의 경우 leaflet-rotate에서 제공하는 rotate 컨트롤 뿐만 아니라 그 밑에 input 컨트롤을 붙여 각도 값을 입력받도록 했다. 그렇기 때문에 각도 값을 동적으로 관리해야 했다.
const [bearing, setBearing] = useState(0); function onRotate(e) { // e.sourceTarget === map setBearing(e?.sourceTarget?.getBearing()); } return ( <Map {...} rotate touchRotate bearing={bearing} rotateControl={{ closeOnZeroBearing: false, position: 'topleft', }} onRotate={onRotate} /> )
하지만 이렇게 관리할 경우 지도가 다시 렌더링되지 않아서 지도의 줌 변경이나 위치 이동 같은 이벤트가 발생해야만 재렌더링 되는 현상이 발생했다. 그렇다고 각도값으로 key를 변경할 경우 지도 전체의 렌더링이 너무 심하게 발생해 깜빡임이 심했다.
const [bearing, setBearing] = useState(0); const [map, setMap] = useState(null); function onRotate(e) { const value = e?.sourceTarget?.getBearing(); setBearing(value); map.setBearing(value); } const mapRef = useCallback((x) => { setMap?.(x?.leafletElement); }, []); return ( <Map {...} rotate touchRotate bearing={bearing} rotateControl={{ closeOnZeroBearing: false, position: 'topleft', }} onRotate={onRotate} ref={mapRef} /> )
그래서 map ref를 가져와 직접 변경하도록 설정했다. state를 사용하는 bearing은 input에서 표시해야 하기 때문에 제거할 수 없어 동시에 수정하도록 했다.
leaflet 1.7 버전을 사용하고 있고, react-leaflet 2.8.0 버전을 사용하고 있기 때문에 onLoad와 같은 함수가 동작하지 않아서 어쩔 수 없이 mapRef를 사용해 객체를 가져왔다.
이렇게 하면 leaflet에서 각도를 변경할 수 있게 된다.
처음에 leaflet-rotate 0.2.5버전을 사용했는데 마커 클릭시 마커가 클릭되지 않고 자꾸 지도가 움직이는 이슈가 발생했었다. 0.2.8 버전으로 업그레이드 하니 해당 버그가 사라졌다.
leaflet에서 각도를 변경하고자 하는 사람들에게 도움이 되었으면 좋겠다.
leaflet을 안 쓰는 게 더 좋을 것 같지만..
'기록이라도 하자' 카테고리의 다른 글
Netlify Git Branch 이름 기준으로 배포 무시하기 (netlify ignore) (1) 2023.10.17 빌드시 메모리 부족으로 빌드 실패하는 현상 해결 (1) 2023.10.07 드러커 엑서사이즈 (0) 2023.10.02 장고 다국어 i18n 적용 (0) 2023.09.22 센트리 로컬 환경에서 사용할 수 있게 설정하기 (0) 2023.08.02