[Leaflet] Zoom levels



Zoom Levels

 - Leaflet은 위도, 경도 그리고 zoom level로 작동을 한다.

 - 줌 레벨이 낮으면 지도가 대륙 전체를 표시하게되며, 줌 레벨이 높으면 지도가 도시의 세부 모습을 표시하게 된다.

 - 줌 레벨의 작동방식을 이해하려면 먼저 측지학에 대한 기본 지식이 필요하다.



지구의 모양

 - 줌 레벨 0으로 잠긴 간단한 지도를 살펴보자.

<!DOCTYPE html>
<html>
<head>
<title>Zoom Levels Tutorial - Leaflet</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" type="image/x-icon" href="docs/images/favicon.ico" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA==" crossorigin=""></script>
<style>
html, body { height: 100%; margin: 0; }
#map { width: 600px; height: 400px; }
</style>
</head>
<body>
<div id='map'></div>
<script>
/**
* 최소/최대 Zoom 레벨 0으로 셋팅
*/
var map = L.map('map', {
minZoom: 0,
maxZoom: 0
});
var cartodbAttribution = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, &copy; <a href="https://carto.com/attribution">CARTO</a>';
/**
* 타일 레이어 생성 및 맵에 추가
*/
var positron = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', {
attribution: cartodbAttribution
}).addTo(map);
/**
* [0, 0] 위치에 Zoom레벨 0으로 보여지도록 셋팅
*/
map.setView([0, 0], 0);
</script>
</body>
</html>



"전체 지구"는 가로 256픽셀, 세로 256픽셀 이미지 하나일 뿐이다.


하지만 진짜 지구는 사각형이 아니다. 오히려 지구는 구형과 비슷한것과 비슷한 불규칙한 모양을 가지고 있다.


그래서 우리는 지구가 대부분 둥글다고 가정한다. 평평하게 만들기 위해 가상의 원통을 주위에 놓고 펴고서 정사각형으로 보이도록 자른다.


위 방법은 평면에서 지구 표면을 표시하는 유일한 방법이 아니며, 장점과 단점이 다른 수백가지의 방법이 있다. 아래 6분짜리 비디오는 주제에 대해서 잘 설명해 주고 있다.
(한국어 자막이 있어 보기 좋다.)

측지학, 지도투영 및 좌표계와 같은 것은 많이 어렵다.(이 튜토리얼의 범위를 벗어남)

지구가 정사각형이라고 가정하는것이 항상 옮은것은 아니지만 대부분의 경우 충분히 잘 작동하고 일이 더 간단해지며 Leaflet (및 기타 지도 라이브러리)의 속도를 높일 수 있다.



2의 거듭제곱

지금은 지구가 정사각형 이라고 가정해 보겠다.

zoom level 0 에서 지구를 표현하면 가로/세로가 256 픽셀이다.

zoom level 1로 변경하면 가로/세로가 두배가 되고 256x256 픽셀 이미지 4대로 표현될 수 있다.


각 zoom level은 4개의 타일로 분할되고 크기가 2배가 되어 면적이 4배가 된다.
 - 가장자리의 길이는 tileSize 옵션으로 제공된다.
 - 지구의 가로/세로 길이는 256 * 2(zoomlevel) 픽셀이 된다.


이러한 줌은 계속되어 타일 서비스는 적용 범위에 따라 zoom level 18 까지 제공되며, 이정도면 타일당 몇개의 도시 블록을 보기에 충분하다.



(Scale) 규모에 대한 참고사항

원통형 투영을 사용할 때의 단점 중에 하나는 배율이 일정하지 않고 특히 낮은 zoom level 에서 거리나 크기를 측정할 수 없다는 것이다.

기술적 용어로, 리플릿이 사용하는 원통형 투영은 등치형(모형을 보존함)이지만 등치형(거리를 보존하지 않음)은 아니며, 등치영역(적도 근처의 사물이 지금보다 작게 나타나므로 영역을 보존하지 않음)은 아니다.

지도에 L.Control.Scale 을 추가하여 적도에서 60도 북쪽으로 이동 시 축척 비율이 두배가 되는 것을 볼 수 있다.

<!DOCTYPE html>
<html>
<head>
<title>Zoom Levels Tutorial - Leaflet</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" type="image/x-icon" href="docs/images/favicon.ico" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA==" crossorigin=""></script>
<style>
html, body { height: 100%; margin: 0; }
#map { width: 600px; height: 400px; }
</style>
</head>
<body>
<div id='map'></div>
<script>
var map = L.map('map', {
minZoom: 0,
maxZoom: 1,
dragging: false // 드래그 사용 못하도록 수정
});
var cartodbAttribution = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, &copy; <a href="https://carto.com/attribution">CARTO</a>';
var positron = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', {
attribution: cartodbAttribution
}).addTo(map);
/**
* 지도에 축척비율 컨트롤레이어 추가
*/
L.control.scale({maxWidth: 150}).addTo(map);
/**
* 2초마다 지도의 위치를 변경한다.
*
* PS.
* Zoom이 변경되지 않았음에도 지구의 윗부분과 아래의 부분에 대한 축척이 다른걸 알 수 있다.
* 현재는 Zoom 레벨이 0이다보니 축척의 차이가 크지만 Zoom 레벨이 높은경우 축척이 크게 차이나지 않는다.
*/
setInterval(function(){
map.setView([0, 0], 0, {duration: 1, animate: true});
setTimeout(function(){
map.setView([60, 0], 0, {duration: 1, animate: true});
}, 2000);
}, 4000);
map.setView([0, 0], 0);
</script>
</body>
</html>



zoom 컨트롤

leaflet 지도는 zoom 레벨을 컨트롤 하는 여러가지 방법이 있지만, 가장 확실한 방법은 setZoom() 을 사용하는 것이다.
 - map.setZoom(0); 을 실행 시 zoom레벨이 0으로 셋팅된다.

<!DOCTYPE html>
<html>
<head>
<title>Zoom Levels Tutorial - Leaflet</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" type="image/x-icon" href="docs/images/favicon.ico" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA==" crossorigin=""></script>
<style>
html, body { height: 100%; margin: 0; }
#map { width: 600px; height: 400px; }
</style>
</head>
<body>
<div id='map'></div>
<script>
var map = L.map('map', {
minZoom: 0,
maxZoom: 1,
dragging: false
});
var cartodbAttribution = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, &copy; <a href="https://carto.com/attribution">CARTO</a>';
var positron = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', {
attribution: cartodbAttribution
}).addTo(map);
/**
* 2초 간격으로 줌레벨을 0 / 1 로 변경한다.
*/
setInterval(function(){
map.setZoom(0);
setTimeout(function(){
map.setZoom(1);
}, 2000);
}, 4000);
/**
* 현재의 줌 레벨을 보여주는 확장 컨트롤레이어 클래스 제작
*/
var ZoomViewer = L.Control.extend({
/**
* onAdd (L.Control 클래스를 확장 시 재정의 할 수 있다. )
* - 컨트롤을 위한 DOM 요소를 반환해야 하며, 적절한 map이벤트를 추가해야 한다.
* control.addTo(map) 에서 호출한다.
*/
onAdd: function(){
var gauge = L.DomUtil.create('div');
gauge.style.width = '200px';
gauge.style.background = 'rgba(255,255,255,0.5)';
gauge.style.textAlign = 'left';
map.on('zoomstart zoom zoomend', function(ev){
gauge.innerHTML = 'Zoom level: ' + map.getZoom();
})
return gauge;
}
});
/**
* 커스텀 컨트롤레이어 객체 생성 후 지도에 추가
*/
(new ZoomViewer).addTo(map);
map.setView([0, 0], 0);
</script>
</body>
</html>


zoom을 셋팅하는 다른 방법은 아래와 같다.

1. setView(center, zoom) - 지도 중심을 설정한다.

2. flyTo(center, zoom) - setView와 비슷하지만 부드러운 애니메이션으로 이동한다.

3. zoomIn()/zoomIn(delta) - delta 값의 zoom 확대 (기본값: 1)

4. zoomOut()/zoomOut(delta) - delta 값의 zoom 축소 (기본값: 1)

5. setZoomAround(fixedPoint, zoom) - 포인트를 고정하면서 zoom 레벨을 설정한다. (마우스 스크롤을 이용하여 zoom레벨 설정과 같다)

6. fitBounds(bounds) - 지도의 직사각형 영역에 맞게 zoom을 자동으로 계산한다.



(분수값)부분 확대(Fractional zoom)

Leaflet 1.0.0에 도입된 기능은 부분확대 개념이다. 그 전에는 zoom 레벨이 정수(0, 1, 2 등) 이었지만 이제는 1.5나 1.25와 같은 분수를 사용할 수 있다.

부분 zoom 기능은 기본적으로 비활성화 되어있으며, 활성화 하려면 지도의 zoomSnap 옵션을 사용하면 된다.
 - zoomSnap 기본값은 1이며, 값은 0, 1, 2와 같은 값도 셋팅 가능하다.
 - zoomSnap을 0.5로 설정 시 zoom레벨은 0 / 0.5 / 1 / 1.5 이런식이 될 것이다.
 - zoomSnap을 0.1로 설정 시 zoom레벨은 0 / 0.1 / 0.2 와 같이 될 것이다.

<!DOCTYPE html>
<html>
<head>
<title>Zoom Levels Tutorial - Leaflet</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" type="image/x-icon" href="docs/images/favicon.ico" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA==" crossorigin=""></script>
<style>
html, body { height: 100%; margin: 0; }
#map { width: 600px; height: 400px; }
</style>
</head>
<body>
<div id='map'></div>
<script>
var map = L.map('map', {
minZoom: 0,
maxZoom: 1,
zoomSnap: 0.25, // 줌레벨 (배수)단계를 0.25로 설정한다.
dragging: false
});
var cartodbAttribution = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, &copy; <a href="https://carto.com/attribution">CARTO</a>';
var positron = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', {
attribution: cartodbAttribution
}).addTo(map);
/**
* Zoom 레벨 0에서 1까지 단계별로 수행한다.
*/
function zoomCycle(){
map.setZoom(0);
timeouts = [];
timeouts.push(setTimeout(function(){ map.setZoom(0.25); }, 1000));
timeouts.push(setTimeout(function(){ map.setZoom(0.50); }, 2000));
timeouts.push(setTimeout(function(){ map.setZoom(0.75); }, 3000));
timeouts.push(setTimeout(function(){ map.setZoom(1); }, 4000));
timeouts.push(setTimeout(function(){ map.setZoom(0.75); }, 5000));
timeouts.push(setTimeout(function(){ map.setZoom(0.50); }, 6000));
timeouts.push(setTimeout(function(){ map.setZoom(0.25); }, 7000));
}
zoomCycle(); // 화면 로딩시 한번 수행
/**
* zoomCycle 시간에 맞춰 (Interval)주기적으로 수행할 수 있도록 셋팅한다.
*/
var zoomingInterval = setInterval(zoomCycle, 8000);
var ZoomViewer = L.Control.extend({
onAdd: function(){
var container= L.DomUtil.create('div');
var gauge = L.DomUtil.create('div');
container.style.width = '200px';
container.style.background = 'rgba(255,255,255,0.5)';
container.style.textAlign = 'left';
map.on('zoomstart zoom zoomend', function(ev){
gauge.innerHTML = 'Zoom level: ' + map.getZoom();
})
container.appendChild(gauge);
return container;
}
});
(new ZoomViewer).addTo(map);
map.setView([0, 0], 0);
</script>
</body>
</html>


Leaflet은 zoom레벨은 가장 가까운 유효한 zoomSnap 값으로 이동한다.
 - 예를들어 zoomSnap 0.25 로 설정 후 map.setZoom(0.8) 수행 시 0.07로 zoom레벨이 셋팅된다.
 - 이 상황은 map.fitBounds(bounds) 와 스크린에 손가락 제스쳐를 이용해 zoom레벨 변경시에도 동일하다.

zoomSnap을 0으로 맞출 수 있는데 이런경우 zoom레벨 변경 시 zoomSnap 값에 영향을 받지 않게된다.

추가적으로 zoomSnap과 관련있는 중요한 zoomDelta 옵션이 있다.
 - 이 옵션은 zoom 버튼을 눌러 zoom in/out 동작 시 zoom레벨을 얼마나 변화할지에 대한 설정이다.
 - 키보드 + / - 버튼을 누를 때도 마찬가지다.


마우스 휠의 zoom컨트롤은 wheelPxPerZoomLevel 옵션으로 zoom 확대/축소 속도 조절이 가능하다.


아래의 예제를 보고 다음의 zoom 레벨의 변화를 확인할 수 있다.

 * 터치스크린의 핀치줌 (손가락 확대/축소)

 * 마우스 휠을 이용한 확대/축소

 * 박스 zoom (키보드 Shift 키를 누른상태에서 마우스 드래그)

 * 왼쪽 위 확대/축소 버튼

<!DOCTYPE html>
<html>
<head>
<title>Zoom Levels Tutorial - Leaflet</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" type="image/x-icon" href="docs/images/favicon.ico" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA==" crossorigin=""></script>
<style>
html, body { height: 100%; margin: 0; }
#map { width: 600px; height: 400px; }
</style>
</head>
<body>
<div id='map'></div>
<script>
var map = L.map('map', {
minZoom: 0,
maxZoom: 18,
zoomSnap: 0,
zoomDelta: 0.25 // zoomIn / zoomOut 수행 후 줌레벨 얼마나 변경될지 셋팅
});
var cartodbAttribution = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, &copy; <a href="https://carto.com/attribution">CARTO</a>';
var positron = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', {
attribution: cartodbAttribution
}).addTo(map);
var ZoomViewer = L.Control.extend({
onAdd: function(){
var container= L.DomUtil.create('div');
var gauge = L.DomUtil.create('div');
container.style.width = '200px';
container.style.background = 'rgba(255,255,255,0.5)';
container.style.textAlign = 'left';
map.on('zoomstart zoom zoomend', function(ev){
gauge.innerHTML = 'Zoom level: ' + map.getZoom();
})
container.appendChild(gauge);
return container;
}
});
(new ZoomViewer).addTo(map);
map.setView([0, 0], 0);
</script>
</body>
</html>



참고: https://leafletjs.com/examples/zoom-levels/


댓글