vworld, openlayers api를 활용한 지도 웹사이트
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VWorld Map with Styles</title>
<script type="text/javascript"
src="https://map.vworld.kr/js/vworldMapInit.js.do?version=2.0&apiKey=${apiKey}"></script>
<script src="https://cdn.rawgit.com/openlayers/openlayers.github.io/master/en/v5.3.0/build/ol.js"></script>
<script src="https://code.jquery.com/jquery-2.2.3.min.js"></script>
<link rel="stylesheet" type="text/css" href="./styles.css">
</head>
<body>
<div id="sidebar">
<form id="searchForm" action="#" class="form_data" onsubmit="return false;">
<input type="hidden" name="page" value="1" />
<input type="hidden" name="type" value="PLACE" />
<input type="hidden" name="size" value="1000" />
<input type="hidden" name="request" value="search" />
<input type="hidden" name="apiKey" value="${apiKey}" />
<div style="display: flexbox; align-items: center;">
<input type="text" id="searchValue" name="query" placeholder="여의도" />
<button id="searchButton" class="btn" onclick="search();">검색</button>
<span> 결과 : </span>
<span id="res-cnt"></span>
</div>
</form>
<div style="margin-top: 20px;">
<button class="btn" onclick="changeMapStyle('base')">2D 배경지도</button>
<button class="btn" onclick="changeMapStyle('white')">2D 백지도</button>
<button class="btn" onclick="changeMapStyle('night')">2D 야간지도</button>
<button class="btn" onclick="changeMapStyle('hybrid')">위성지도</button>
</div>
<ul id="searchResults"></ul>
</div>
<div id="map-container">
<div id="map" style="height: 100%;"></div>
<div class="zoom-level" id="zoom-level">Zoom: 13</div>
<div class="scale-line" id="scale-line"></div>
</div>
<script src="https://cdn.rawgit.com/openlayers/openlayers.github.io/master/en/v5.3.0/build/ol.js"></script>
<script src="https://code.jquery.com/jquery-2.2.3.min.js"></script>
<script src="scripts.js"></script>
</body>
</html>
body {
margin: 0;
font-family: Arial, sans-serif;
display: flex;
height: 100vh;
}
#sidebar {
width: 300px;
background-color: #f0f0f0;
padding: 20px;
box-sizing: border-box;
overflow-y: auto;
}
#map-container {
flex: 1;
position: relative;
}
#map {
width: 100%;
height: 100%;
}
#searchResults {
list-style: none;
padding: 0;
max-height: calc(100% - 220px);
overflow-y: auto;
margin-top: 10px;
border-top: 1px solid #ccc;
padding-top: 10px;
}
#searchResults li {
padding: 10px;
cursor: pointer;
border-bottom: 1px solid #ddd;
}
#searchResults li:hover {
background-color: rgb(173, 252, 226);
}
#searchForm {
margin-bottom: 10px;
}
#searchValue {
font-size: 16px;
width: 250px;
margin-right: 10px;
margin-bottom: 10px;
padding: 5px;
border: 1px solid #ccc;
border-radius: 3px;
}
.btn {
margin-bottom: 5px;
padding: 7px 20px;
background-color: #007bff;
color: #fff;
border: none;
cursor: pointer;
border-radius: 3px;
}
#res-cnt {
font-weight: bold;
margin-left: 10px;
}
strong {
font-size: 18px;
}
.overlayElement {
background-color: #3399CC;
border: 2px solid white;
color: white;
padding: 10px;
border-radius: 5px;
cursor: pointer;
}
.overlayElement:hover {
background-color: #286090;
}
.closeButton {
position: absolute;
top: 5px;
right: 5px;
color: white;
background-color: rgba(0, 0, 0, 0.5);
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
line-height: 18px;
text-align: center;
cursor: pointer;
}
.closeButton:hover {
background-color: rgba(0, 0, 0, 0.7);
}
.ol-zoom-in, .ol-zoom-out {
background-color: rgba(0, 0, 0, 1) !important;
}
.ol-zoom {
bottom: 65px !important;
left: auto !important;
top: auto !important;
}
.ol-zoomslider {
background-color: rgba(255, 255, 255, 0.8) !important;
border: 1px solid #ccc !important;
right: 20px !important;
bottom: 40px !important;
left: auto !important;
top: auto !important;
}
.zoom-level {
position: absolute;
bottom: 15px;
right: 20px;
background: rgba(255, 255, 255, 0.8);
padding: 5px;
border: 1px solid #ccc;
border-radius: 3px;
font-size: 14px;
}
.scale-line {
left: auto !important;
top: auto !important;
bottom: 10px !important;
right: 20px !important;
background: rgba(255, 255, 255, 0.8);
padding: 5px;
border: 1px solid #ccc;
border-radius: 3px;
font-size: 14px;
}
.ol-scale-line-inner {
font-size: 16px;
}
const vworldKey = 'apiKey';
let mapStyles = {
base: new ol.layer.Tile({
title: 'VWorld Base Map',
visible: true,
source: new ol.source.XYZ({
url: `http://api.vworld.kr/req/wmts/1.0.0/${vworldKey}/Base/{z}/{y}/{x}.png`
})
}),
white: new ol.layer.Tile({
title: 'VWorld White Map',
visible: false,
source: new ol.source.XYZ({
url: `http://api.vworld.kr/req/wmts/1.0.0/${vworldKey}/white/{z}/{y}/{x}.png`
})
}),
night: new ol.layer.Tile({
title: 'VWorld Night Map',
visible: false,
source: new ol.source.XYZ({
url: `http://api.vworld.kr/req/wmts/1.0.0/${vworldKey}/midnight/{z}/{y}/{x}.png`
})
}),
hybrid: new ol.layer.Tile({
title: 'VWorld Hybrid Map',
visible: false,
source: new ol.source.XYZ({
url: `http://api.vworld.kr/req/wmts/1.0.0/${vworldKey}/Hybrid/{z}/{y}/{x}.png`
})
})
};
let currentMapStyle = 'base';
const map = new ol.Map({
target: 'map',
layers: [
mapStyles.base,
mapStyles.white,
mapStyles.night,
mapStyles.hybrid
],
view: new ol.View({
center: ol.proj.transform([126.925535, 37.525101], 'EPSG:4326', 'EPSG:3857'),
zoom: 13,
minZoom: 7, // 최소 줌 레벨
maxZoom: 19 // 최대 줌 레벨
}),
controls: ol.control.defaults().extend([
new ol.control.ZoomSlider(),
new ol.control.ScaleLine({
target: 'scale-line',
units: 'metric'
})
])
});
map.getControls().forEach(function (control) {
if (control instanceof ol.control.ZoomSlider) {
control.setTarget(document.getElementById('map-container'));
}
});
let features = [];
let styleCache = [];
let search = function () {
const searchValue = document.getElementById("searchValue");
if (searchValue.value === "") {
alert('검색 내용이 없습니다.');
return false;
}
$.ajax({
type: "get",
url: "http://api.vworld.kr/req/search",
data: $('#searchForm').serialize(),
dataType: 'jsonp',
async: false,
success: function (data) {
let searchResults = $('#searchResults');
searchResults.empty(); // 기존 결과를 지우기
features = []; // 기존 피처를 초기화
for (let o in data.response.result.items) {
if (o == 0) {
move(data.response.result.items[o].point.x * 1, data.response.result.items[o].point.y * 1);
}
// 검색 결과를 li 태그로 변환하여 ul에 추가
let item = data.response.result.items[o];
let listItem = $(`<litoken interpolation">${item.point.x}, ${item.point.y}, 1)"><strong>${item.title}</strong> <p>${item.address.road || item.address.parcel}</p></li>`);
searchResults.append(listItem);
// Feature 객체에 저장하여 활용
features[o] = new ol.Feature({
geometry: new ol.geom.Point(ol.proj.transform([item.point.x * 1, item.point.y * 1], 'EPSG:4326', "EPSG:3857")),
title: item.title,
parcel: item.address.parcel,
road: item.address.road,
category: item.category,
point: item.point
});
features[o].set("id", item.id);
let iconStyle = new ol.style.Style({
image: new ol.style.Icon({
anchor: [0.5, 10],
anchorXUnits: 'fraction',
anchorYUnits: 'pixels',
src: 'http://map.vworld.kr/images/ol3/marker_blue.png'
})
});
features[o].setStyle(iconStyle);
}
let vectorSource = new ol.source.Vector({
features: features
});
let vectorLayer = new ol.layer.Vector({
source: vectorSource
});
vectorLayer.set("vectorLayer", "search_vector");
map.getLayers().forEach(function (layer) {
if (layer.get("vectorLayer") == "search_vector") {
map.removeLayer(layer);
}
});
map.addLayer(vectorLayer);
console.log(data.response.result.items.length);
const resCnt = document.getElementById('res-cnt');
resCnt.innerText = data.response.result.items.length;
},
error: function (xhr, stat, err) {}
});
}
let move = function (x, y, mode = 0) {
let _center = ol.proj.transform([x, y], 'EPSG:4326', "EPSG:3857");
map.getView().setCenter(_center);
setTimeout(fnMoveZoom(mode), 500);
}
function fnMoveZoom(mode) {
let zoom = map.getView().getZoom();
if (zoom < 14) { // 검색을 한 경우
map.getView().setZoom(14);
}
if (mode) { // li 눌러서 해당 위치를 자세히 보는 경우
map.getView().setZoom(18);
}
}
map.on("click", function (evt) {
let pixel = evt.pixel;
map.forEachFeatureAtPixel(pixel, function (feature) {
const title = feature.get("title"); // 이름 정보
const address = feature.get("road") || feature.get("parcel"); // 주소 정보
if (title) {
var overlayElement = document.createElement("div");
overlayElement.setAttribute("class", "overlayElement");
overlayElement.setAttribute("style", "background-color: #3399CC; border: 2px solid white; color:white");
overlayElement.setAttribute("onclick", "deleteOverlay('" + feature.get("id") + "')");
overlayElement.innerHTML = `<strong>${title}</strong><p>주소 : ${address}</p>`; // 이름, 주소 정보 출력
let overlayInfo = new ol.Overlay({
id: feature.get("id"),
element: overlayElement,
offset: [0, -70],
position: ol.proj.transform([feature.get("point").x * 1, feature.get("point").y * 1], 'EPSG:4326', "EPSG:3857")
});
if (feature.get("id") != null) {
map.removeOverlay(map.getOverlayById(feature.get("id")));
}
map.addOverlay(overlayInfo);
}
});
});
let deleteOverlay = function (id) {
map.removeOverlay(map.getOverlayById(id));
}
// Update zoom level display
const zoomLevelElement = document.getElementById('zoom-level');
const updateZoomLevel = () => {
zoomLevelElement.innerText = 'Zoom: ' + map.getView().getZoom().toFixed(2);
};
map.getView().on('change:resolution', updateZoomLevel);
updateZoomLevel();
// Function to change map style based on button click
function changeMapStyle(style) {
currentMapStyle = style;
// 모든 레이어 숨기기
for (let key in mapStyles) {
mapStyles[key].setVisible(false);
}
// 선택된 스타일 레이어 보이기
mapStyles[style].setVisible(true);
}