클러스터 맵 추가
모든 캠핑장을 보여주는 index 페이지에 클러스터 맵을 추가 해보자. mapbox를 사용한다.
https://docs.mapbox.com/mapbox-gl-js/example/cluster/
위 링크로 들어가서 필요한 코드를 붙여넣는다.
이 코드는 이미 boilerplate에 포함되어있다.
<link href="https://api.mapbox.com/mapbox-gl-js/v3.0.1/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v3.0.1/mapbox-gl.js"></script>
clusterMap.js라는 파일을 새로 만들어서 스크립트 부분을 붙여 넣는다. 참고로 나는 맵 스타일을 light-v10 버전으로 바꿨다.
mapboxgl.accessToken = mapToken;
const map = new mapboxgl.Map({
container: 'map',
// Choose from Mapbox's core styles, or make your own style with Mapbox Studio
style: 'mapbox://styles/mapbox/light-v10',
center: [-103.5917, 40.6699],
zoom: 3
});
map.on('load', () => {
// Add a new source from our GeoJSON data and
// set the 'cluster' option to true. GL-JS will
// add the point_count property to your source data.
map.addSource('earthquakes', {
type: 'geojson',
// Point to GeoJSON data. This example visualizes all M1.0+ earthquakes
// from 12/22/15 to 1/21/16 as logged by USGS' Earthquake hazards program.
data: 'https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson',
cluster: true,
clusterMaxZoom: 14, // Max zoom to cluster points on
clusterRadius: 50 // Radius of each cluster when clustering points (defaults to 50)
});
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'earthquakes',
filter: ['has', 'point_count'],
paint: {
// Use step expressions (https://docs.mapbox.com/style-spec/reference/expressions/#step)
// with three steps to implement three types of circles:
// * Blue, 20px circles when point count is less than 100
// * Yellow, 30px circles when point count is between 100 and 750
// * Pink, 40px circles when point count is greater than or equal to 750
'circle-color': [
'step',
['get', 'point_count'],
'#51bbd6',
100,
'#f1f075',
750,
'#f28cb1'
],
'circle-radius': [
'step',
['get', 'point_count'],
20,
100,
30,
750,
40
]
}
});
map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: 'earthquakes',
filter: ['has', 'point_count'],
layout: {
'text-field': ['get', 'point_count_abbreviated'],
'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
'text-size': 12
}
});
map.addLayer({
id: 'unclustered-point',
type: 'circle',
source: 'earthquakes',
filter: ['!', ['has', 'point_count']],
paint: {
'circle-color': '#11b4da',
'circle-radius': 4,
'circle-stroke-width': 1,
'circle-stroke-color': '#fff'
}
});
// inspect a cluster on click
map.on('click', 'clusters', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['clusters']
});
const clusterId = features[0].properties.cluster_id;
map.getSource('earthquakes').getClusterExpansionZoom(
clusterId,
(err, zoom) => {
if (err) return;
map.easeTo({
center: features[0].geometry.coordinates,
zoom: zoom
});
}
);
});
// When a click event occurs on a feature in
// the unclustered-point layer, open a popup at
// the location of the feature, with
// description HTML from its properties.
map.on('click', 'unclustered-point', (e) => {
const coordinates = e.features[0].geometry.coordinates.slice();
const mag = e.features[0].properties.mag;
const tsunami =
e.features[0].properties.tsunami === 1 ? 'yes' : 'no';
// Ensure that if the map is zoomed out such that
// multiple copies of the feature are visible, the
// popup appears over the copy being pointed to.
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
}
new mapboxgl.Popup()
.setLngLat(coordinates)
.setHTML(
`magnitude: ${mag}<br>Was there a tsunami?: ${tsunami}`
)
.addTo(map);
});
map.on('mouseenter', 'clusters', () => {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'clusters', () => {
map.getCanvas().style.cursor = '';
});
});
clusterMap.js에서 mapbox 토큰에 접근할 수 있도록 index.ejs 페이지 최하단에 mapToken을 정의하고 clusterMap.js 스크립트를 포함시킨다.
<script>
const mapToken = '<%-process.env.MAPBOX_TOKEN%>';
</script>
<script src="/javascript/clusterMap.js"></script>
이제 클러스터 맵을 표시하기 위한 기본적인 것들이 준비됐다. index.ejs 파일에서 클러스터 맵 표시를 원하는 위치에 아래 코드를 붙여넣는다. 스타일은 원하는 대로 지정하면 된다. 참고로 width랑 height 설정 안 하면 맵이 안 보인다.
<div id="map" style="margin-top: 10px; width: 100%; height: 500px;"></div>
캠핑장 위치 표시
index.ejs에는 모든 캠핑장을 배열로 가지고 있는 campgrounds 변수가 있다. clusterMap.js에서 campgrounds 변수에 접근할 수 있도록 아래와 같이 index.ejs 파일에 추가해 주자. 추가해도 동작은 안 하는데 차차 알아보자.
<script>
const mapToken = '<%-process.env.MAPBOX_TOKEN%>';
const campgrounds = <%- JSON.stringify(campgrounds) %>;
</script>
clusterMap.js에서 map.on 부분의 데이터를 보면 addSource 안에 data가 있는데 url이 있다. url을 복사해서 브라우저에서 실행해 보자.
features는 배열이고 그 안에 데이터가 매우 많은데 두 개만 보이도록 캡처했다. 이게 지도에 점을 찍어주는 정보임을 알 수 있다. 따라서 features를 키로 하고, index.ejs에서의 campgrounds 배열을 값으로 하여 clusterMap.js에서 접근할 수 있게 해야 한다. 그러니 아래처럼 코드를 고치자.
<script>
const mapToken = '<%-process.env.MAPBOX_TOKEN%>';
const campgrounds = {features: <%- JSON.stringify(campgrounds) %>};
</script>
clusterMap.js에서 map을 선언하는 부분 중 center: [127.5, 37]로 고친다. 맵이 처음 보일 때 중심을 대한민국의 경도와 위도로 설정한 것이다. zoom은 5로 설정했다. zoom이 크면 좁은 지역이 자세히 보인다. 그리고 addSource 안에 data를 campgrounds로 고친다. 'earthquakes'로 되어있는 모든 문자열도 'campgrounds'로 바꾼다. 안 바꿔줘도 작동하지 않는다거나 그러지는 않는다.
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/light-v10',
center: [127.5, 37],
zoom: 5
});
map.on('load', () => {
map.addSource('campgrounds', {
type: 'geojson',
data: campgrounds,
cluster: true,
clusterMaxZoom: 14, // Max zoom to cluster points on
clusterRadius: 50 // Radius of each cluster when clustering points (defaults to 50)
});
위치 클릭 팝업
위치를 클릭하면 캠핑장 이름과 주소를 보여주고, 캠핑장 이름을 클릭하면 상세 페이지로 이동시켜 주는 팝업창을 만들어보자.
아래 코드는 공식 페이지에서 복사한 코드 그대로이고, 이 코드는 하나하나 점 찍혀있는 캠핑장을 클릭했을 때 팝업을 띄워주는 역할을 하는 코드이다. 하지만 이대로 사용하면 원하는 결과가 나오지 않는다. 무슨 일인지 확인하기 위해 e.features[0]을 콘솔에 출력해 보자.
map.on('click', 'unclustered-point', (e) => {
const coordinates = e.features[0].geometry.coordinates.slice();
const mag = e.features[0].properties.mag;
const tsunami =
e.features[0].properties.tsunami === 1 ? 'yes' : 'no';
// Ensure that if the map is zoomed out such that
// multiple copies of the feature are visible, the
// popup appears over the copy being pointed to.
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
}
new mapboxgl.Popup()
.setLngLat(coordinates)
.setHTML(
`magnitude: ${mag}<br>Was there a tsunami?: ${tsunami}`
)
.addTo(map);
});
properties가 비어있는 것을 확인할 수 있다. 우리가 만든 캠핑장 스키마에는 properties 속성이 없기 때문이다. 위 코드를 보면 알겠지만 팝업에 띄울 메시지는 features[인덱스]의 properties에 접근해서 결정한다. features[인덱스]는 캠핑장을 의미한다. 따라서 캠핑장 스키마에 properties 속성을 추가해야 한다.
캠핑장 스키마 자체에 속성을 추가해도 되지만 나는 가상 속성을 추가할 것이다.
캠핑장 스키마가 정의되어 있는 campground.js에서 아래 코드를 추가한다.
CampgroundSchema.virtual('properties').get(function () {
return { popUpHTML:
`<h5><a href="campgrounds/${this._id}">${this.title}</a></h5>
<p>${this.location}</p>`
};
})
clusterMap.js를 아래와 같이 수정한다.
map.on('click', 'unclustered-point', (e) => {
const coordinates = e.features[0].geometry.coordinates.slice();
const popupText = e.features[0].properties.popupText;
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
}
new mapboxgl.Popup()
.setLngLat(coordinates)
.setHTML(popupText)
.addTo(map);
});
근데 실행하고 점을 클릭해 보면 undefined라고 뜰 것이다. 캠핑장을 콘솔에 출력해보자. 속성 목록에 가상 속성 properties가 없다. 그래서 undefined라고 뜨는 것이다. mongoose 문서를 보면 '기본적으로 mongoose는 문서를 JSON으로 변환할 때 가상 속성을 포함하지 않는다'라고 되어 있다. 따라서 가상 속성을 포함해 주는 옵션을 지정해야 한다.
캠핑장 스키마가 정의되어 있는 파일을 아래 코드처럼 수정한다.
const opts = { toJSON: { virtuals: true } };
...
const CampgroundSchema = new Schema({
title: {
type: String,
required: true
},
...
}, opts);
clusterMap.js 파일에서 점의 색도 바꾸고 크기도 바꿀 수 있다. 나는 클러스터 되지 않은 점이 너무 작은 것 같아서 'circle-radius'를 7로 바꿨다. 원래는 4였다.
map.addLayer({
id: 'unclustered-point',
type: 'circle',
source: 'campgrounds',
filter: ['!', ['has', 'point_count']],
paint: {
'circle-color': '#11b4da',
'circle-radius': 7,
'circle-stroke-width': 1,
'circle-stroke-color': '#fff'
}
});
'웹 프로그래밍' 카테고리의 다른 글
[YelpCamp 프로젝트] 보안 (0) | 2023.12.11 |
---|---|
[Yelpcamp 프로젝트] 디자인 수정 (0) | 2023.12.10 |
[YelpCamp 프로젝트] 지도 추가 (1) | 2023.12.08 |
[Yelpcamp 프로젝트] 이미지 편집 (0) | 2023.12.07 |
[Yelpcamp 프로젝트] 이미지 파일 (1) | 2023.12.07 |