이전에는 서울지리데이터.geojson
을 google map
위에 overlay Layer
로 올리고 따릉이 대여소 위치
를 표현하였다. 그리고 오늘은 따릉이를 어디서 빌려서, 어디에 반납하는지 그 흐름을 표 나타내 보려고 한다.
오늘의 작업을 위해서는 두가지 단계가 필요하다.
<div></div>
안에 지도 레이어를 채워 넣는 개념이기 때문에 단순히 새로운 div (레이어)
를 position : absolute
를 하게 되면 지도에 대한 동작이 실행이 안된다. 그렇기에 google에서 제공하는 overlayLayer object
를 이용해서 내부적으로 레이어를 쌓아야 한다. google map data layer
를 이용하여 뿌린다. data layer
는 geojson
데이터를 뿌린다. (API 문서 링크)fitbound
는 주어진 위, 경도 값을 이용하여 경계BOX를 생성하여 해당 BOX를 확대하는 것이다. 이 때 이 BOX의 초기 값을 잡기 위해서 extend
를 해준다. (API 문서 링크)d3
로 생성한 svg
내에서는 좌표를 pixel로 변환을 해주어야 한다. geojosn
객체 셍성geojson
데이터 뿌리기fitbound
를 하기 위해 대여/반납 장소의 위 경도 값을 extend
해준 뒤 화면에 맞게 확대geojson 을 이용해서 데이터를 뿌려주는 이유는 실무를 진행할 때 geojson의 properties 라는 속성에 위,경도에 대한 정보를 담고, 꺼낼 수 있다는 장점이 있기 때문에 geojson으로 생성하여 진행하였다.
여기서 data
는 따릉이 대여/반납 데이터이다. (1개 데이터 이용)
// geojson을 생성한다.
// geojson의 구성에 맞게 데이터를 생성함.
function createGeoJson(data) {
const fromGeojson = {coordinates: [data.from_lng, data.from_lat], type: "Point"}
const toGeojson = {coordinates: [data.to_lng, data.to_lat], type: "Point"}
const initFeature = {type: "FeatureCollection", features: []};
const createFeature = (data, geojson, type) => initFeature.features.push({
type: "Feature",
properties: {layerType: "rent", data: data, type},
geometry: geojson
});
createFeature(data.from_nm, fromGeojson, "from");
createFeature(data.to_nm, toGeojson, "to");
return {feature: initFeature, loc: {from: [data.from_lat, data.from_lng], to: [data.to_lat, data.to_lng]}};
}
fitbound(data)
함수가 실행되면 geojson
을 생성하고, map.data.addGeoJson(geojson객체)
를 해줌으로서 지도 위에 단순히 Marker만 뿌려준다.
map.data.setStyle
를 하면 Marker를 custom 할 수 있다. 나는 svg
이미지를 이용하여 뿌린다.
그리고 fitbound
를 하기 위해 bound
객체를 생성해 주고 대여/반납 위 경도 값을 extend
해준 뒤 map.fitBounds()
를 해준다.
function fitBound(data) {
const {feature, loc} = createGeoJson(data);
// 데이터 레이어로 Marker 뿌리기
map.data.addGeoJson(feature);
// Marker style Setting.
map.data.setStyle((feature) => {
const getType = feature.getProperty("type");
return {
icon: {
url: 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(createSvg(getType)),
// anchor: new window.google.maps.Point(20, 56)
},
zIndex: 10,
visible: true
};
});
// fitBounds 하기 위해 경계 Box 사이즈 확장(등록?)
const bound = new google.maps.LatLngBounds();
bound.extend(new google.maps.LatLng(loc.from[0], loc.from[1]))
bound.extend(new google.maps.LatLng(loc.to[0], loc.to[1]))
// bounds를 fit 하게 맞추기
map.fitBounds(bound);
}
아래에 진행되는 모든 작업은 fitBounds가 된 이후에 확장된 화면에서 진행된다.
그리고 삼각함수에서 사용되는 x,y 좌표 값은 위경도 값이 아닌 위경도를 화면의 pixel 값으로 변환한 다음에 진행해야만 한다. svg위에 그리는 것이디 때문에..!!위경도 값을 변환하는 것은 code를 보면 쉽게 알 수 있다!
d3 에서 curve
를 그리기 위해서는 세가지 좌표가 필요하다.
위 사진에서 보여지는 것 처럼 h
의 경우 에는 사용자가 임의로 정의하면 되는 값이다.
사진 처럼 직선의 경우에는 center
의 좌표를 쉽게 게산할 수 있지만, 2차원 평면에서 구하기 위해서는 삼각함수를 이용해야만 한다.
curve
를 그리기 위해 필요한 값들대각선(d)의 중심 좌표를 알았으면 이제 curve
를 그릴때 필요한 center
값 아래 사진에서는 h
를 구해야한다. 이것 또한 삼각비를 이해하면 쉽게 유도해 낼 수 있다!!
x'
과 y'
값을 위에서 구한 대각선(d) 의 중심 좌표
에 더해주면 된다 !
아래 사진에서는 - 를 해주고 있지만 사실 그렇게 크게 중요한 요소는 아닌거 같다. 자 대고 정확하게 그림을 그려 내는 게 아니라 curve 느낌만 주면 되니까..!
d3
로 지도 위에 svg
생성할 수 있게 overlayLayer
객체 생성d3
로 svg 그릴 Canvas생성 사실 여기서 부터 코드를 어떻게 설명해야할지 몰라서 위에서 작업 순서를 작성하였다.
d3
의 기본 기능은 나보다 더 깊은 이해를 하고 있다고 생각하고 코드를 설명해보자면...
참고 1
참고 2
// 1. overlay Layer 객체 생성!!
const overlay = new google.maps.OverlayView();
let layer, projection, boundObj, bound_sw, bound_ne;
// 1-2 overlay Layer 객체가 생성되면 자동으로 실행되는 내장 함수
// overlayLayer에 flow 를 그릴 도화지를 펼치는 것
overlay.onAdd = function () {
layer = d3.select(this.getPanes().overlayLayer)
.append("div")
.attr("class", "stations")
.attr("id", "arrowCanvas")
};
// 1-2 overlay Layer 객체가 생성되면 자동으로 실행되는 내장 함수
// 펼친 도화지에 이제 그림으르 그린다.
overlay.draw = function () {
// 만약에 이전에 누가 쓰던 도화지 있으면 찢어버려!!
if (d3.select('#arrowCanvas svg')) {
d3.select('#arrowCanvas svg').remove();
}
projection = this.getProjection();
// 위에 설명한 위,경도 값을 pixel 로 convert 해서 모서리 값 도출 ---- (참고. 1)
// flow를 그릴 svg 사이즈 그려야지
boundObj = map.getBounds();
//bound_sw = 화면의 왼쪽 아래 꼭지점 (south-west)
bound_sw = projection.fromLatLngToDivPixel(boundObj.getSouthWest());
//bound_ne = 화면의 오른 쪽 위 꼭지점 (north-east)
bound_ne = projection.fromLatLngToDivPixel(boundObj.getNorthEast());
// 제 도화지 이름과 사이즈는 이거에요! ----- (참고. 2)
const canvas = layer.append("svg")
.attr("id", 'arrowCanvasSvg')
.style("left", bound_sw.x + 'px')
.style("top", bound_ne.y + 'px')
.style('width', bound_ne.x - bound_sw.x)
.style('height', bound_sw.y - bound_ne.y)
const arrowGroupCanvas = canvas.append("g").attr("id", "arrowGroup");
/**
* transform(d)에서 위 경도 값을 pixel 값으로 convert 해 준다.
* 상황에 따라 다양한 작업이 이루어지지만 여기선 단순히 convert 용도
*/
let arrowCanvas = arrowGroupCanvas.selectAll('g')
.data([data])
.each(d => transform(d))
.enter()
.append('g')
.each(d => transform(d));
/**
* cala(d) 함수를 통해 위에서 도출한 curve를 그릴 때 필요한 [center 좌표] 를 도출한다.
*/
let lineArrow = arrowCanvas.append("path")
.attr("d", d => {
const {from, to, gap} = calc(d);
const lineCurve = d3.line().curve(d3.curveBasis);
return lineCurve([[from.x, from.y], [gap.x, gap.y], [to.x, to.y]]);
}).style('stroke', '#498eff')
.style('stroke-width', 3)
.attr("fill-opacity", 0);
};
calc
function에서는 위에서 유도한 식을 그대로 사용하였기 때문에 이해하기에 어려움은 없을 거 같다. 다만 scaleValue
라는 녀석을 모를 수 있는데, 이 녀석은 사용자가 zoom level에 따라 대각선 (d) 길이
가 달라지게 된다. 그렇기에 x'
과 y'
의 값도 달라지게 된다. 이를 보정해주는 zoom Level에 따른 보정값 이라고 생각하면 된다.
/**
* transform(d)에서 위 경도 값을 pixel 값으로 convert 해 준다.
* 상황에 따라 다양한 작업이 이루어지지만 여기선 단순히 convert 용도
*/
function transform(d) {
const convertToPixel = (projection, lat, lng) => {
const latLng = new google.maps.LatLng(lat, lng);
return projection.fromLatLngToDivPixel(latLng);
};
const fr_pixel = convertToPixel(projection, d.from_lat, d.from_lng);
const to_pixel = convertToPixel(projection, d.to_lat, d.to_lng);
d.fr_pix = fr_pixel;
d.to_pix = to_pixel;
}
/**
* cala(d) 함수를 통해 위에서 도출한
* curve를 그릴 때 필요한 [center 좌표] 를 도출한다.
*/
function calc(d) {
const start_point = d.fr_pix;
const end_point = d.to_pix;
const from_point = {x: start_point.x - bound_sw.x, y: start_point.y - bound_ne.y};
const to_point = {x: end_point.x - bound_sw.x, y: end_point.y - bound_ne.y}
const getDistance = (x, y) => Math.sqrt(x ** 2 + y ** 2);
const getDegree = (x, y) => (Math.atan2(y, x) * 180 / Math.PI);
const toRadian = (degree) => degree * (Math.PI / 180);
const distance_x = Math.abs(to_point.x - from_point.x);
const distance_y = Math.abs(to_point.y - from_point.y);
const distance_d = getDistance(distance_x, distance_y);
const scaleValue = (distance_d / 600).toFixed(4);
const angle = getDegree(distance_x, distance_y); // radian to degree
const center = {x: (from_point.x + to_point.x) / 2, y: (from_point.y + to_point.y) / 2};
const cyx = center.x + (150 * Math.cos(toRadian(angle - 90)) * scaleValue);
const cyy = center.y + (150 * Math.sin(toRadian(angle - 90)) * scaleValue);
return {
gap: {x: cyx, y: cyy}
, from: from_point
, to: to_point,
}
}
단순히 라인을 그리는 것 까지 진행을 하였고, 그 외에 animation 효과와 마지막 화살표 모양은 따로 설명없이 코드만 남겼다. (전체 코드)
삼각함수를 정말 오랜만에 접하다보니 계속 어버버 했던거 같다.
개발이 어려운게 아니라 삼각함수를 통해 값을 유도해내는게 너무 오래 걸렸다..ㅠㅠ 공부좀 하자..
부장님 말씀으로는 개발자는 이정도 수학은 할 줄 알아야 된다며 어려워하는 나를 보고 측은한 눈빛을 보내셨지만 나는 해냈다..!! ㅋㅋ
아무튼 끝내고 나니 별거 아니고 확실히 머릿속에 각인되어 다음번에 비슷한 로직을 짜야할 상황이 생기면 누구보다 더 빨리 이해하고 진행할 수 있을 거 같단 자심감을 갖고 또 다음 개발을 하러.. 출발..!