mirror of
https://github.com/morgan9e/KorailMap
synced 2026-04-13 16:04:07 +09:00
init
This commit is contained in:
30
static/gis.html
Normal file
30
static/gis.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<meta charset="UTF-8">
|
||||
<title>열차위치</title>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" >
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<link rel="shortcut icon" href="https://gis.korail.com/korailTalk//favicon.ico">
|
||||
<script src='https://gis.korail.com/korailTalk/libs/babel/babel.min.js'></script>
|
||||
<script src='https://gis.korail.com/korailTalk/libs/geojson/geojson.min.js'></script>
|
||||
<link rel="stylesheet" type="text/css" href="ol-helper.css" />
|
||||
<link rel="stylesheet" href="ol.css" />
|
||||
<script src='ol.js'></script>
|
||||
<script>
|
||||
const params = {
|
||||
lon : null,
|
||||
lat : null,
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='map' style="position: absolute; width: 100%; height: 100%">
|
||||
<div id="popup" class="ol-popup"></div>
|
||||
</div>
|
||||
<script src='ol-helper.js'></script>
|
||||
<script src='main.js'></script>
|
||||
</body>
|
||||
</html>
|
||||
662
static/main.js
Normal file
662
static/main.js
Normal file
@@ -0,0 +1,662 @@
|
||||
document.addEventListener("DOMContentLoaded", async function () {
|
||||
initMap();
|
||||
await initLayer();
|
||||
initView();
|
||||
// initMapLegend();
|
||||
mouseEvent();
|
||||
});
|
||||
|
||||
let map;
|
||||
|
||||
let originalFetch = window.fetch;
|
||||
async function newFetch(url, options = {}) {
|
||||
let headers = options.headers ||
|
||||
{
|
||||
'user-agent': "Mozilla/5.0 (Linux; Android 12; SM-N976N Build/SP1A.210812.016; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/126.0.6478.133 Mobile Safari/537.36 korailtalk AppVersion/6.3.3",
|
||||
'x-requested-with': "com.korail.talk",
|
||||
'sec-ch-ua-platform': "Android",
|
||||
"referer": "https://gis.korail.com/korailTalk/entrance"
|
||||
};
|
||||
let f = originalFetch('https://proxy.devpg.net', {
|
||||
headers: {
|
||||
'X-Proxy-URL': 'https://gis.korail.com' + url,
|
||||
'X-Proxy-Header': JSON.stringify(headers)
|
||||
}
|
||||
});
|
||||
console.log(`fetch(https://gis.korail.com${url}, headers=${JSON.stringify(headers)}`, f);
|
||||
return f
|
||||
}
|
||||
window.fetch = newFetch;
|
||||
|
||||
function initMap() {
|
||||
map = new ol.Map({
|
||||
target: 'map',
|
||||
view: new ol.View({
|
||||
minZoom: 5,
|
||||
maxZoom: 15.9,
|
||||
extent: [120, 30, 135, 45].toEPSG3857(),
|
||||
}),
|
||||
controls: ol.control.defaults.defaults({
|
||||
attribution: false,
|
||||
zoom: false,
|
||||
rotate: false,
|
||||
}),
|
||||
interactions: ol.interaction.defaults.defaults({
|
||||
altShiftDragRotate: false,
|
||||
pinchRotate: false,
|
||||
}).extend([new ol.interaction.DblClickDragZoom()])
|
||||
});
|
||||
if (params.lon && params.lat) {
|
||||
map.getView().setCenter([params.lon, params.lat].toEPSG3857());
|
||||
map.getView().setZoom(11);
|
||||
} else {
|
||||
map.getView().setCenter([127.35, 36.50].toEPSG3857());
|
||||
map.getView().setZoom(6);
|
||||
}
|
||||
map.getTargetElement().style.background = '#b5dbf3';
|
||||
}
|
||||
|
||||
const trainColors = {
|
||||
ktx: '#1B4298',
|
||||
itx: '#C10230',
|
||||
etc: '#54565A',
|
||||
srt:'#651C5C'
|
||||
}
|
||||
Object.freeze(trainColors);
|
||||
|
||||
const layerStyles = {
|
||||
line: {
|
||||
'stroke-width': 2.25,
|
||||
'stroke-color': '#68a7d5',
|
||||
},
|
||||
station: {
|
||||
'circle-radius': [
|
||||
'match', ['get', 'grade', 'string'],
|
||||
'0', ["interpolate", ["linear"],["zoom"], 5, 3, 15, 6],
|
||||
'1', ["interpolate", ["linear"],["zoom"], 5, 3, 15, 6],
|
||||
'2', ["interpolate", ["linear"],["zoom"], 5, 2, 15, 5],
|
||||
'3', ["interpolate", ["linear"],["zoom"], 5, 2, 15, 5],
|
||||
0
|
||||
],
|
||||
'circle-stroke-width': [
|
||||
'match', ['get', 'grade', 'string'],
|
||||
'0', ["interpolate", ["linear"],["zoom"], 5, 1, 15, 3],
|
||||
'1', ["interpolate", ["linear"],["zoom"], 5, 1, 15, 3],
|
||||
'2', ["interpolate", ["linear"],["zoom"], 5, 1, 15, 3],
|
||||
'3', 0,
|
||||
0
|
||||
],
|
||||
'circle-stroke-color': '#68a7d5',
|
||||
'circle-fill-color': [
|
||||
'match', ['get', 'grade', 'string'],
|
||||
'3', '#68a7d5',
|
||||
'white'
|
||||
]
|
||||
},
|
||||
station_label: {
|
||||
'text-value': ['get', 'name', 'string'],
|
||||
'text-stroke-color': "#68a7d5",
|
||||
'text-stroke-width': 3,
|
||||
'text-fill-color': "#ffffff",
|
||||
'text-font': [
|
||||
'match', ['get', 'grade', 'number'],
|
||||
0, '17px SUIT-M',
|
||||
1, '15px SUIT-M',
|
||||
2, '13px SUIT-M',
|
||||
3, '11px SUIT-M',
|
||||
'0px SUIT-M'
|
||||
],
|
||||
'text-offset-x': ['get', 'text_offset_x'],
|
||||
'text-offset-y': ['get', 'text_offset_y'],
|
||||
},
|
||||
trainStyle: (feature, resolution) => {
|
||||
const {
|
||||
trn_no: trnNo,
|
||||
trn_case: trnCase,
|
||||
trn_opr_cd: trnOpr,
|
||||
trn_clsf: trnClass,
|
||||
icon,
|
||||
bearing
|
||||
} = feature.getProperties();
|
||||
const coords = feature.getGeometry().getCoordinates()
|
||||
const overlabs = map.getLayer('train').getSource().getFeaturesAtCoordinate(coords).sort((a, b) => a.get('trn_no') - b.get('trn_no'));
|
||||
|
||||
return new ol.style.Style({
|
||||
image: new ol.style.Icon({
|
||||
src: `icon/train/ic_${icon}.svg`,
|
||||
rotation: bearing,
|
||||
scale: 0.6 + ((map.getView().getZoomForResolution(resolution) - 10) / 10),
|
||||
declutterMode: 'none',
|
||||
}),
|
||||
text: (map.getView().getZoom() >= 11) ? new ol.style.Text({
|
||||
font: '14px SUIT-B',
|
||||
stroke: new ol.style.Stroke({
|
||||
color: '#f3f3f3',
|
||||
width: 1,
|
||||
}),
|
||||
fill: new ol.style.Fill({
|
||||
color: trainColors[trnClass],
|
||||
}),
|
||||
offsetX: 5 + map.getView().getZoom(),
|
||||
offsetY: 0 + overlabs.indexOf(feature) * 20,
|
||||
overflow: true,
|
||||
declutterMode: 'none',
|
||||
textAlign: 'left',
|
||||
textBaseline: 'middle',
|
||||
text: `${trnCase} ${trnNo}`,
|
||||
}) : null,
|
||||
zIndex: map.get('trainClicked')?.getId() == feature.getId() ? Infinity :
|
||||
trnOpr == 15 ? overlabs.reverse().indexOf(feature) :
|
||||
0
|
||||
})
|
||||
}
|
||||
};
|
||||
Object.freeze(layerStyles);
|
||||
|
||||
async function initLayer() {
|
||||
try {
|
||||
map.startLoadingEffect();
|
||||
|
||||
await Promise.all([
|
||||
map.addBaseTileLayer('korean'),
|
||||
|
||||
initRailLayers(),
|
||||
initStationLayers(),
|
||||
|
||||
initTrainLayer(),
|
||||
]);
|
||||
} catch {
|
||||
|
||||
} finally {
|
||||
map.finishLoadingEffect();
|
||||
}
|
||||
}
|
||||
|
||||
async function initRailLayers() {
|
||||
return new Promise(async (resolve) => {
|
||||
await Promise.all([
|
||||
// 고속선 정보
|
||||
map.addRailLayer('/api/data/rail_ktx', {
|
||||
name: 'rail_ktx',
|
||||
style: layerStyles.line,
|
||||
zIndex: 1,
|
||||
}),
|
||||
// 준고속선 정보
|
||||
map.addRailLayer('/api/data/rail_semi', {
|
||||
name: 'rail_semi',
|
||||
style: layerStyles.line,
|
||||
zIndex: 1,
|
||||
}),
|
||||
// 일반선 정보
|
||||
map.addRailLayer('/api/data/rail_normal', {
|
||||
name: 'rail_normal',
|
||||
style: layerStyles.line,
|
||||
zIndex: 1,
|
||||
}),
|
||||
// 물류선(추가) 정보
|
||||
map.addRailLayer('/api/data/rail_logis', {
|
||||
name: 'rail_logis',
|
||||
style: layerStyles.line,
|
||||
zIndex: 1,
|
||||
}),
|
||||
]);
|
||||
|
||||
resolve();
|
||||
})
|
||||
}
|
||||
|
||||
async function initStationLayers() {
|
||||
const response = await fetch('/api/data/station');
|
||||
map.set('korailStation', Object.freeze(await response.json()));
|
||||
|
||||
const filteredStation = (binary) => {
|
||||
const temp = JSON.parse(JSON.stringify(map.get('korailStation')));
|
||||
temp.features = temp.features.filter((f) => (f.properties.text_offset_x && f.properties.text_offset_y) !== null);
|
||||
temp.features = temp.features.filter((f) => Number(f.properties.shown_layer) & binary);
|
||||
return ol.source.Vector.fromGeoJSON(temp);
|
||||
}
|
||||
|
||||
return new Promise(async (resolve) => {
|
||||
await Promise.all([
|
||||
// 고속역 정보
|
||||
map.addPointsLayer({
|
||||
name: 'KTX_Station',
|
||||
source: filteredStation(0b100000),
|
||||
style: layerStyles.station,
|
||||
labelStyleFunction: layerStyles.station_label,
|
||||
zIndex: 2,
|
||||
}),
|
||||
// 준고속역 정보
|
||||
map.addPointsLayer({
|
||||
name: 'SemiExpress_Station',
|
||||
source: filteredStation(0b010000),
|
||||
style: layerStyles.station,
|
||||
labelStyleFunction: layerStyles.station_label,
|
||||
zIndex: 2,
|
||||
}),
|
||||
// 일반역 정보
|
||||
map.addPointsLayer({
|
||||
name: 'Normal_Station',
|
||||
source: filteredStation(0b001000),
|
||||
style: layerStyles.station,
|
||||
labelStyleFunction: layerStyles.station_label,
|
||||
zIndex: 2,
|
||||
}),
|
||||
// 여객역 정보
|
||||
map.addPointsLayer({
|
||||
name: 'Passenger_Station',
|
||||
source: filteredStation(0b000100),
|
||||
style: layerStyles.station,
|
||||
labelStyleFunction: layerStyles.station_label,
|
||||
zIndex: 2,
|
||||
}),
|
||||
// 물류역 정보
|
||||
map.addPointsLayer({
|
||||
name: 'Dist_Station',
|
||||
source: filteredStation(0b000001),
|
||||
style: layerStyles.station,
|
||||
labelStyleFunction: layerStyles.station_label,
|
||||
zIndex: 2,
|
||||
}),
|
||||
]);
|
||||
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function initRefreshDOM() {
|
||||
const dom = document.createElement('div')
|
||||
dom.id = 'refresh-info'
|
||||
dom.style.position = 'absolute'
|
||||
dom.style.bottom = '0px'
|
||||
dom.style.right = '0px'
|
||||
dom.style.display = 'flex'
|
||||
dom.style.flexDirection = 'column'
|
||||
dom.style.justifyContent = 'space-evenly'
|
||||
dom.style.alignItems = 'end'
|
||||
dom.style.margin = '20px'
|
||||
dom.style.padding = 'env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)'
|
||||
dom.style.boxSizing = 'border-box'
|
||||
dom.style.touchAction = 'none'
|
||||
|
||||
const button = document.createElement('input')
|
||||
button.type = 'image'
|
||||
button.src = 'icon/refresh.svg'
|
||||
button.alt = '새로고침'
|
||||
button.onclick = (event) => {
|
||||
refreshTrainLayer()
|
||||
|
||||
const target = event.currentTarget
|
||||
target.classList.add('rotate')
|
||||
setTimeout(() => {
|
||||
target.classList.remove('rotate');
|
||||
}, 500);
|
||||
}
|
||||
button.style.width = '1rem'
|
||||
button.style.height = '1rem'
|
||||
button.style.background = 'transparent'
|
||||
button.style.border = '0px'
|
||||
button.style.borderRadius = '50%'
|
||||
button.style.boxShadow = '0px 0px 5px gainsboro'
|
||||
button.style.background = 'white'
|
||||
button.style.margin = '5px 0px'
|
||||
button.style.padding = '5px'
|
||||
|
||||
const text = document.createElement('span')
|
||||
text.style.fontSize = 'small'
|
||||
text.style.transition = 'color 0.5s'
|
||||
text.style.color = 'black'
|
||||
|
||||
dom.appendChild(button)
|
||||
dom.appendChild(text)
|
||||
map.getTargetElement().appendChild(dom)
|
||||
|
||||
updateRefreshText();
|
||||
}
|
||||
|
||||
const refreshIterator = {
|
||||
interval: undefined,
|
||||
function: undefined,
|
||||
}
|
||||
|
||||
function syncIterator() {
|
||||
const zoom = map.getView().getZoom();
|
||||
const intZoom = Math.floor(zoom);
|
||||
const intervalSec = 60 - ((intZoom - 5) * 5.5);
|
||||
|
||||
refreshIterator.interval = intervalSec * 1000;
|
||||
clearInterval(refreshIterator.function);
|
||||
refreshIterator.function = setInterval(refreshTrainLayer, refreshIterator.interval);
|
||||
}
|
||||
|
||||
function updateRefreshText() {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0'); // 월은 0부터 시작하므로 +1 필요
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||
|
||||
const text = document.querySelector('div#refresh-info > span')
|
||||
text.textContent = `(${year}.${month}.${day}. ${hours}:${minutes}:${seconds}) 기준`;
|
||||
|
||||
text.classList.add('blink')
|
||||
setTimeout(() => {
|
||||
text.classList.remove('blink');
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function refreshTrainLayer() {
|
||||
const getBufferedExtent = (view) => {
|
||||
const extent = view.calculateExtent();
|
||||
const resolution = view.getResolution();
|
||||
const buffer = 100 * resolution;
|
||||
const bufferedExtent = [
|
||||
extent[0] - buffer,
|
||||
extent[1] - buffer,
|
||||
extent[2] + buffer,
|
||||
extent[3] + buffer,
|
||||
]
|
||||
|
||||
return bufferedExtent;
|
||||
}
|
||||
|
||||
return new Promise(async (resolve) => {
|
||||
const bufferedExtent = getBufferedExtent(map.getView());
|
||||
const bbox = bufferedExtent.toWGS84().join(',');
|
||||
const filter = `${params.trn ? `trnNo=${params.trn}` : `bbox=${bbox}`}`;
|
||||
if (params.trn && params.date) {
|
||||
const date = params.date
|
||||
|
||||
const now = new Date()
|
||||
const yyyy = now.getFullYear()
|
||||
const mm = (now.getMonth() +1).toString().padStart(2, '0')
|
||||
const dd = now.getDate().toString().padStart(2, '0')
|
||||
const yyyymmdd = `${yyyy}${mm}${dd}`;
|
||||
|
||||
if (date !== yyyymmdd) {
|
||||
return
|
||||
}
|
||||
}
|
||||
const response = await fetch(`/api/train?${filter}`);
|
||||
const geojson = await response.json();
|
||||
|
||||
const newFeatures = ol.source.Vector.fromGeoJSON(geojson).getFeatures();
|
||||
if (params?.trn) {
|
||||
newFeatures.forEach((f) => {
|
||||
f.set('icon', 'train');
|
||||
f.set('bearing', 0);
|
||||
})
|
||||
} else {
|
||||
newFeatures.forEach((f) => {
|
||||
f.set('icon', `${f.get('trn_clsf')}${(f.get('delay') > 20 ? '_delay' : '')}`);
|
||||
})
|
||||
}
|
||||
const trainSource = map.getLayer('train')?.getSource();
|
||||
|
||||
trainSource?.clear();
|
||||
trainSource?.addFeatures(newFeatures);
|
||||
|
||||
if (map.get('trainClicked') && map.isShowingOverlay()) {
|
||||
const past = map.get('trainClicked');
|
||||
const now = newFeatures.find((feature) => (feature.get('trn_no') === past.get('trn_no')));
|
||||
if (now) {
|
||||
const [pastX, pastY] = past.getGeometry().getCoordinates();
|
||||
const nowCoords = now.getGeometry().getCoordinates();
|
||||
const [nowX, nowY] = nowCoords;
|
||||
if ( !(pastX === nowX && pastY === nowY) ) {
|
||||
map.getLayer('train').once('postrender',() => {
|
||||
map.getOverlay().setPosition(nowCoords);
|
||||
map.getView().animate({
|
||||
center: nowCoords,
|
||||
duration: 500,
|
||||
});
|
||||
showPopup(now);
|
||||
})
|
||||
}
|
||||
} else {
|
||||
map.hideOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
//sendMessageToApp();
|
||||
|
||||
resolve()
|
||||
})
|
||||
.then(updateRefreshText)
|
||||
}
|
||||
|
||||
function reloadTrainLayer() {
|
||||
refreshTrainLayer()
|
||||
syncIterator()
|
||||
}
|
||||
|
||||
async function initTrainLayer() {
|
||||
return new Promise(async (resolve) => {
|
||||
map.addPointsLayer({
|
||||
name: 'train',
|
||||
style: layerStyles.trainStyle,
|
||||
zIndex: 9,
|
||||
declutter: true,
|
||||
});
|
||||
map.on('pointerdrag', map.hideOverlay)
|
||||
map.on('moveend', reloadTrainLayer)
|
||||
initRefreshDOM();
|
||||
resolve()
|
||||
});
|
||||
}
|
||||
|
||||
const showPopup = (feature) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
map.set('trainClicked', feature);
|
||||
|
||||
const coordinate = feature.getGeometry().getFlatCoordinates();
|
||||
const popup = map.getOverlay();
|
||||
|
||||
try {
|
||||
const { trn_no, trn_case, trn_clsf, dpt_stn_nm, dpt_pln_dttm, arv_stn_nm, arv_pln_dttm, now_stn, next_stn, delay } = feature.getProperties();
|
||||
const dpt_time = `${dpt_pln_dttm.slice(8,10)}:${dpt_pln_dttm.slice(10,12)}`;
|
||||
const arv_time = `${arv_pln_dttm.slice(8,10)}:${arv_pln_dttm.slice(10,12)}`;
|
||||
const now_loc = now_stn && next_stn ? `${now_stn}${next_stn}` : '';
|
||||
|
||||
html = `
|
||||
<div class="train-popup">
|
||||
<div class="train-popup-name">
|
||||
<span class="train-popup-no" style="color: #0066b3;">${trn_case} ${trn_no}</span>
|
||||
</div>
|
||||
<hr/>
|
||||
<div class="train-popup-info">
|
||||
<div><title>운행구간</title><content>${dpt_stn_nm}(${dpt_time}) ~ ${arv_stn_nm}(${arv_time})</content></div>
|
||||
<div><title>현재위치</title><content>${now_loc}</content></div>
|
||||
<div usage="delay"><title>예상지연</title><content style="${(delay > 0) ? 'color: #159aff; font-weight: bold' : ''}">${delay} 분</content></div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
popup.element.innerHTML = html;
|
||||
|
||||
if (delay === null) {
|
||||
popup.element.querySelector('div[usage="delay"]')?.remove()
|
||||
popup.element.querySelector('div[usage="delay-reason"]')?.remove()
|
||||
}
|
||||
|
||||
popup.setPosition(coordinate);
|
||||
map.getView().animate({
|
||||
center: coordinate,
|
||||
duration: 500,
|
||||
})
|
||||
map.showOverlay();
|
||||
|
||||
popup.element.style.top = `-${popup.element.offsetHeight + 20}px`;
|
||||
popup.element.style.left = `-${popup.element.offsetWidth / 2}px`;
|
||||
resolve();
|
||||
}
|
||||
catch {
|
||||
map.hideOverlay();
|
||||
reject();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function mouseEvent() {
|
||||
map.set('trainClicked', null);
|
||||
|
||||
const options = {
|
||||
layerFilter: (layer) => layer.get('name') === 'train',
|
||||
hitTolerance: 20
|
||||
};
|
||||
|
||||
map.on('singleclick', async (e) => {
|
||||
if (map.getView().getInteracting() || map.getView().getAnimating()) {
|
||||
map.getTargetElement().style.cursor = "grabbing";
|
||||
return;
|
||||
}
|
||||
if (map.get('trainClicked') && map.getFeaturesAtPixel(e.pixel, options).length > 0 && map.get('trainClicked') === map.getFeaturesAtPixel(e.pixel, options)[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (map.get('trainClicked') !== null) {
|
||||
map.set('trainClicked', null);
|
||||
map.hideOverlay();
|
||||
}
|
||||
|
||||
const features = map.getFeaturesAtPixel(e.pixel, options);
|
||||
if (features.length > 0 && showPopup(features[0])) {
|
||||
map.showOverlay();
|
||||
} else {
|
||||
map.hideOverlay();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createLegend() {
|
||||
const controllerDOM = document.createElement('div');
|
||||
controllerDOM.id = 'viewController'
|
||||
controllerDOM.style.position = 'absolute';
|
||||
controllerDOM.style.width = '100%';
|
||||
controllerDOM.style.bottom = '0px';
|
||||
controllerDOM.style.minWidth = '100px';
|
||||
controllerDOM.style.display = 'flex';
|
||||
controllerDOM.style.flexDirection = 'row';
|
||||
controllerDOM.style.justifyContent = 'space-evenly';
|
||||
controllerDOM.style.marginBottom = '30px';
|
||||
controllerDOM.style.touchAction = 'none';
|
||||
|
||||
const buttons = ["고속", "일반", "광역", "화물"]
|
||||
|
||||
buttons.forEach((name, index) => {
|
||||
const buttonSize = '50px';
|
||||
const button = document.createElement('div');
|
||||
button.style.display = 'block';
|
||||
button.style.justifyContent = 'space-between';
|
||||
button.style.width = buttonSize;
|
||||
button.style.height = buttonSize
|
||||
button.style.lineHeight = buttonSize;
|
||||
button.style.borderRadius = buttonSize;
|
||||
button.style.background = 'darkgray';
|
||||
button.style.color = 'white';
|
||||
button.style.textAlign = 'center';
|
||||
button.style.boxShadow = '1px 1px 5px gray';
|
||||
button.textContent = name;
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
map.set('viewType', index);
|
||||
viewcontrol(map.get('viewType'));
|
||||
})
|
||||
|
||||
controllerDOM.appendChild(button);
|
||||
});
|
||||
|
||||
return controllerDOM;
|
||||
}
|
||||
|
||||
function initView() {
|
||||
const view = map.getView();
|
||||
|
||||
if (params?.trn && params?.date) {
|
||||
map.once('rendercomplete', () => {
|
||||
const trains = map.getLayer('train').getSource().getFeatures();
|
||||
const myTrain = trains.find((t) => t.get('trn_no') == params.trn);
|
||||
|
||||
if (myTrain) {
|
||||
showPopup(myTrain);
|
||||
if (!(params.lon && params.lat)) {
|
||||
view.setCenter(myTrain.getGeometry().flatCoordinates)
|
||||
view.setZoom(11);
|
||||
}
|
||||
} else {
|
||||
// createModalPopup('열차가 운행중이지 않습니다.');
|
||||
setTimeout(() => window.location.reload(), 60000)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createModalPopup(_content) {
|
||||
const backgroundDOM = document.createElement('div');
|
||||
backgroundDOM.style.position = 'absolute';
|
||||
backgroundDOM.style.display = 'flex';
|
||||
backgroundDOM.style.width = '100%';
|
||||
backgroundDOM.style.height = '100%';
|
||||
backgroundDOM.style.background = 'rgba(0,0,0,0.5)';
|
||||
backgroundDOM.style.userSelect = 'none';
|
||||
backgroundDOM.id = 'modal-popup';
|
||||
|
||||
|
||||
const popupDOM = document.createElement('div');
|
||||
popupDOM.style.margin = 'auto';
|
||||
popupDOM.style.padding = '10px';
|
||||
popupDOM.style.borderRadius = '5px';
|
||||
popupDOM.style.background = 'white';
|
||||
popupDOM.style.textAlign = 'center';
|
||||
//popupDOM.style.zIndex = 1;
|
||||
popupDOM.innerText = _content;
|
||||
|
||||
backgroundDOM.appendChild(popupDOM);
|
||||
document.body.appendChild(backgroundDOM);
|
||||
}
|
||||
|
||||
const webviewPlatform = Object.freeze({
|
||||
IOS_WEBVIEW: "iOS WebView",
|
||||
IOS_BROWSER: "iOS Browser",
|
||||
AOS_WEBVIEW: "Android WebView",
|
||||
AOS_BROWSER: "Android Browser",
|
||||
UNKNOWN: "Unknown Platform"
|
||||
});
|
||||
|
||||
function getWebViewPlatform() {
|
||||
var userAgent = navigator.userAgent || navigator.vendor || window.opera;
|
||||
|
||||
if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) {
|
||||
if (window.webkit && window.webkit.messageHandlers) {
|
||||
return webviewPlatform.IOS_WEBVIEW;
|
||||
}
|
||||
return webviewPlatform.IOS_BROWSER;
|
||||
}
|
||||
|
||||
if (/android/i.test(userAgent)) {
|
||||
if (window.Android) {
|
||||
return webviewPlatform.AOS_WEBVIEW;
|
||||
}
|
||||
return webviewPlatform.AOS_BROWSER;
|
||||
}
|
||||
|
||||
return webviewPlatform.UNKNOWN;
|
||||
}
|
||||
|
||||
function sendMessageToApp() {
|
||||
const osPlatform = getWebViewPlatform()
|
||||
const message = "gis refresh"
|
||||
switch (osPlatform) {
|
||||
case webviewPlatform.IOS_WEBVIEW :
|
||||
case webviewPlatform.IOS_BROWSER :
|
||||
window.webkit.messageHandlers.refresh.postMessage(message)
|
||||
return;
|
||||
case webviewPlatform.AOS_WEBVIEW :
|
||||
case webviewPlatform.AOS_BROWSER :
|
||||
Android.showMessage(message);
|
||||
return;
|
||||
default :
|
||||
return;
|
||||
}
|
||||
}
|
||||
142
static/ol-helper.css
Normal file
142
static/ol-helper.css
Normal file
@@ -0,0 +1,142 @@
|
||||
/*font*/
|
||||
@font-face {
|
||||
font-family: "SUIT-B";
|
||||
src: url("SUIT-Regular.ttf");
|
||||
font-weight: bold;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "SUIT-M";
|
||||
src: url("SUIT-Regular.ttf");
|
||||
font-weight: medium;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "SUIT-R";
|
||||
src: url("SUIT-Regular.ttf");
|
||||
font-weight: regular;
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: SUIT-R !important;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
touch-action: pan-x;
|
||||
}
|
||||
body {
|
||||
margin:0;
|
||||
}
|
||||
html:has(#map), body:has(#map) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#map {
|
||||
background: #171933;
|
||||
}
|
||||
|
||||
.ol-overlay-container {
|
||||
position:absolute;
|
||||
background-color: #fff;
|
||||
box-shadow: 5px 5px 3px rgba(0,0,0,0.2);
|
||||
border-radius: 10px;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
.ol-overlay-container:after {
|
||||
top: 100%;
|
||||
border: solid transparent;
|
||||
content: " ";
|
||||
height: 0;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
.ol-overlay-container:after {
|
||||
border-top-color: #fff;
|
||||
border-width: 10px;
|
||||
left: 50%;
|
||||
margin-left: -10px;
|
||||
}
|
||||
|
||||
|
||||
.train-popup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 15px 27px;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
|
||||
font-family: SUIT;
|
||||
color: red;
|
||||
}
|
||||
.train-popup-name {
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
align-items: end;
|
||||
}
|
||||
.train-popup-name > .train-popup-no{
|
||||
font-size: 25px;
|
||||
font-weight: bold;
|
||||
font-stretch: normal;
|
||||
font-style: normal;
|
||||
line-height: normal;
|
||||
letter-spacing: -0.5px;
|
||||
text-align: left;
|
||||
color: black;
|
||||
}
|
||||
.train-popup-name > .train-popup-no {
|
||||
color: #496dff;
|
||||
}
|
||||
.train-popup-pair {
|
||||
color: #9b9b9b;
|
||||
}
|
||||
|
||||
.train-popup hr {
|
||||
display: block;
|
||||
margin: 8px 0px;
|
||||
}
|
||||
.train-popup-info {
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.train-popup-info > div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
margin: 0px;
|
||||
}
|
||||
.train-popup-info > div:not(:last-child) {
|
||||
margin: 7px 0px;
|
||||
}
|
||||
|
||||
.train-popup-info > div > title {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-right: 15px;
|
||||
}
|
||||
.train-popup-info > div > content {
|
||||
display: contents;
|
||||
}
|
||||
.train-popup-info > div > span {
|
||||
font-size: x-small;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% { color: lightgray }
|
||||
100% { color: black }
|
||||
}
|
||||
.blink {
|
||||
animation: blink 0.5s
|
||||
}
|
||||
@keyframes rotate {
|
||||
0% { transform: rotate(0deg) }
|
||||
100% { transform: rotate(-360deg) }
|
||||
}
|
||||
.rotate {
|
||||
animation: rotate 0.5s
|
||||
}
|
||||
772
static/ol-helper.js
Normal file
772
static/ol-helper.js
Normal file
@@ -0,0 +1,772 @@
|
||||
/**
|
||||
* Openlayers Map
|
||||
* author: khy
|
||||
* date: 23.09.07
|
||||
* */
|
||||
|
||||
const proj = {
|
||||
viewProjection: "EPSG:3857",
|
||||
WGS84Projection: "EPSG:4326",
|
||||
};
|
||||
Object.freeze(proj);
|
||||
Array.prototype.toWGS84 = function () {
|
||||
if (this.length === 2) {
|
||||
//coordinate
|
||||
return ol.proj.transform(this, proj.viewProjection, proj.WGS84Projection);
|
||||
} else if (this.length === 4) {
|
||||
//extent
|
||||
return ol.proj.transformExtent(this, proj.viewProjection, proj.WGS84Projection);
|
||||
} else {
|
||||
return this;
|
||||
}
|
||||
};
|
||||
Array.prototype.toEPSG3857 = function () {
|
||||
if (this.length === 2) {
|
||||
//coordinate
|
||||
return ol.proj.transform(this, proj.WGS84Projection, proj.viewProjection);
|
||||
} else if (this.length === 4) {
|
||||
//extent
|
||||
return ol.proj.transformExtent(this, proj.WGS84Projection, proj.viewProjection);
|
||||
} else {
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
ol.Map.prototype.getProxyURL = function (url) {
|
||||
return `${document.location.protocol}//${document.location.host}/proxy/?${url ? url : ""}`;
|
||||
};
|
||||
|
||||
ol.Map.prototype.addBaseTileLayer = function (options) {
|
||||
let host = 'gis.korail.com';
|
||||
function createBaseTileLayer(map) {
|
||||
const proxy = map.getProxyURL();
|
||||
const base = new ol.layer.Tile({
|
||||
name: "base",
|
||||
source: new ol.source.XYZ({
|
||||
tileUrlFunction: (zxy) => {
|
||||
const [z, x, y] = zxy;
|
||||
if (z < 5 || z > 15) {
|
||||
return;
|
||||
}
|
||||
return `https://${host}/tilemap/background/${z}/${x}/${y}.png`;
|
||||
},
|
||||
minZoom: 5,
|
||||
maxZoom: 15,
|
||||
}),
|
||||
zIndex: 0,
|
||||
});
|
||||
|
||||
if (options === "korean") {
|
||||
const label = new ol.layer.Tile({
|
||||
name: "base-label",
|
||||
source: new ol.source.XYZ({
|
||||
tileUrlFunction: (zxy) => {
|
||||
const [z, x, y] = zxy;
|
||||
if (z < 5 || z > 15) {
|
||||
return;
|
||||
}
|
||||
return `https://${host}/tilemap/korean/${z}/${x}/${y}.png`;
|
||||
},
|
||||
minZoom: 5,
|
||||
maxZoom: 15,
|
||||
}),
|
||||
zIndex: 0,
|
||||
});
|
||||
|
||||
const layergroup = new ol.layer.Group({
|
||||
layers: [base, label],
|
||||
});
|
||||
return layergroup;
|
||||
} else {
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
||||
const layer = createBaseTileLayer(this);
|
||||
this.addLayer(layer);
|
||||
|
||||
return layer;
|
||||
};
|
||||
ol.Map.prototype.createBaseLayerButton = function () {
|
||||
const mapElement = this.getTargetElement();
|
||||
|
||||
if (!mapElement.querySelector("button[usage=base]")) {
|
||||
const toggleButton = document.createElement("button");
|
||||
toggleButton.setAttribute("usage", "base");
|
||||
toggleButton.textContent = "";
|
||||
toggleButton.style.position = "absolute";
|
||||
toggleButton.style.top = "0px";
|
||||
toggleButton.style.left = "0px";
|
||||
toggleButton.style.margin = "10px";
|
||||
toggleButton.style.marginLeft = "42px";
|
||||
toggleButton.style.width = "30px";
|
||||
toggleButton.style.height = "30px";
|
||||
toggleButton.style.borderRadius = "50%";
|
||||
toggleButton.style.border = "1px solid gray";
|
||||
toggleButton.style.background = "white";
|
||||
toggleButton.style.color = "black";
|
||||
toggleButton.style.textAlign = "center";
|
||||
|
||||
let status = false;
|
||||
|
||||
const toggleLayer = () => {
|
||||
this.getAllLayers()
|
||||
.filter((l) => l.get("name") !== "base" && l.get("name") !== "base-label" && l.get("name") !== "grid-tile")
|
||||
.forEach((l) => {
|
||||
l.setVisible(status);
|
||||
});
|
||||
|
||||
status = !status;
|
||||
|
||||
toggleButton.style.background = status ? "dodgerblue" : "white";
|
||||
toggleButton.style.color = status ? "white" : "black";
|
||||
|
||||
if (status) {
|
||||
viewChange(null, null);
|
||||
map.getView().setConstrainResolution(true);
|
||||
} else {
|
||||
map.getView().setConstrainResolution(false);
|
||||
viewChange(0, document.querySelector("ul.notice > li"));
|
||||
}
|
||||
};
|
||||
toggleButton.onclick = toggleLayer;
|
||||
|
||||
let zoomLevel = undefined;
|
||||
this.on("moveend", (view) => {
|
||||
const nowZoomLevel = Number(this.getView().getZoom().toFixed());
|
||||
if (zoomLevel !== nowZoomLevel) {
|
||||
zoomLevel = nowZoomLevel;
|
||||
toggleButton.textContent = zoomLevel;
|
||||
}
|
||||
});
|
||||
|
||||
mapElement.appendChild(toggleButton);
|
||||
}
|
||||
};
|
||||
|
||||
ol.Map.prototype.createGridTiles = function () {
|
||||
const mapElement = this.getTargetElement();
|
||||
|
||||
if (!mapElement.querySelector("button[usage=grid-tile]")) {
|
||||
const toggleButton = document.createElement("button");
|
||||
toggleButton.setAttribute("usage", "grid-tile");
|
||||
toggleButton.textContent = "#";
|
||||
toggleButton.style.position = "absolute";
|
||||
toggleButton.style.top = "0px";
|
||||
toggleButton.style.left = "0px";
|
||||
toggleButton.style.margin = "10px";
|
||||
toggleButton.style.width = "30px";
|
||||
toggleButton.style.height = "30px";
|
||||
toggleButton.style.borderRadius = "50%";
|
||||
toggleButton.style.border = "1px solid gray";
|
||||
toggleButton.style.background = "white";
|
||||
toggleButton.style.color = "black";
|
||||
toggleButton.style.textAlign = "center";
|
||||
|
||||
const toggleLayer = () => {
|
||||
const layer = this.getLayer("grid-tile");
|
||||
|
||||
if (layer) {
|
||||
const currentVisible = layer.getVisible();
|
||||
const postVisible = !currentVisible;
|
||||
|
||||
this.getAllLayers()
|
||||
.filter((l) => l.get("name") !== "base" && l.get("name") !== "base-label" && l.get("name") !== "grid-tile")
|
||||
.forEach((l) => {
|
||||
l.setVisible(currentVisible);
|
||||
});
|
||||
|
||||
layer.setVisible(postVisible);
|
||||
|
||||
toggleButton.style.background = postVisible ? "dodgerblue" : "white";
|
||||
toggleButton.style.color = postVisible ? "white" : "black";
|
||||
|
||||
if (postVisible) {
|
||||
viewChange(null, null);
|
||||
} else {
|
||||
viewChange(0, document.querySelector("ul.notice > li"));
|
||||
}
|
||||
}
|
||||
};
|
||||
toggleButton.onclick = toggleLayer;
|
||||
|
||||
mapElement.appendChild(toggleButton);
|
||||
}
|
||||
|
||||
if (!this.getLayer("grid-tile")) {
|
||||
const tileSize = 256;
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = tileSize;
|
||||
canvas.height = tileSize;
|
||||
|
||||
const context = canvas.getContext("2d");
|
||||
context.strokeStyle = "gray";
|
||||
context.textAlign = "center";
|
||||
const lineHeight = 30;
|
||||
context.font = `${lineHeight - 6}px sans-serif`;
|
||||
|
||||
const layer = new ol.layer.WebGLTile({
|
||||
name: "grid-tile",
|
||||
source: new ol.source.DataTile({
|
||||
loader: (z, x, y) => {
|
||||
const half = tileSize / 2;
|
||||
|
||||
context.clearRect(0, 0, tileSize, tileSize);
|
||||
// context.fillStyle = 'rgba(255, 255, 255, 0.7)';
|
||||
// context.fillRect(0, 0, tileSize, tileSize);
|
||||
|
||||
context.fillStyle = "black";
|
||||
context.fillText(`z: ${z}`, half, half - lineHeight);
|
||||
context.fillText(`x: ${x}`, half, half);
|
||||
context.fillText(`y: ${y}`, half, half + lineHeight);
|
||||
|
||||
context.strokeRect(0, 0, tileSize, tileSize);
|
||||
|
||||
const data = context.getImageData(0, 0, tileSize, tileSize).data;
|
||||
|
||||
return new Uint8Array(data.buffer);
|
||||
},
|
||||
}),
|
||||
zIndex: 999,
|
||||
});
|
||||
|
||||
layer.setVisible(false);
|
||||
|
||||
this.addLayer(layer);
|
||||
}
|
||||
};
|
||||
|
||||
ol.source.Vector.fromGeoJSON = function (geojson) {
|
||||
return new ol.source.Vector({
|
||||
features: new ol.format.GeoJSON().readFeatures(geojson, { featureProjection: proj.viewProjection }),
|
||||
format: new ol.format.GeoJSON(),
|
||||
strategy: ol.loadingstrategy.bbox,
|
||||
});
|
||||
};
|
||||
|
||||
ol.Map.prototype.addRegionalLayer = async function () {
|
||||
const regional_lines = new Array(8);
|
||||
|
||||
async function createRegionalLine(url, index) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const json = await response.json();
|
||||
insertLine(json, index);
|
||||
resolve();
|
||||
} catch {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 권역 라인이 포인트 형식으로 되어있어서 라인형식으로 바꾸고 데이터 교체하는 함수
|
||||
function insertLine(json, i) {
|
||||
const coordinates = json.features.map((point) => {
|
||||
const converted = point.geometry.coordinates.toEPSG3857();
|
||||
return converted;
|
||||
});
|
||||
const geomLineString = new ol.geom.LineString(coordinates);
|
||||
const featureLineString = new ol.Feature({ geometry: geomLineString });
|
||||
regional_lines[i] = featureLineString;
|
||||
}
|
||||
|
||||
// $.getJOSN 으로 json 데이터 읽어 드리고 insertLine 은 현재 json 파일이 line 형식이 아닌 포인트
|
||||
// 형식으로
|
||||
// 만들었음
|
||||
// 포인트 형식을 만든 이유는 수정시 포인트 형식이 필요해서 우선 포인트 형식으로 저장
|
||||
|
||||
await Promise.all([
|
||||
createRegionalLine("resource/Regional_point_chungbuk.json", 0),
|
||||
createRegionalLine("resource/Regional_point_gang.json", 1),
|
||||
createRegionalLine("resource/Regional_point_seoul_east.json", 2),
|
||||
createRegionalLine("resource/Regional_point_seoul_west.json", 3),
|
||||
createRegionalLine("resource/Regional_point_daejeon_chungnam.json", 4),
|
||||
createRegionalLine("resource/Regional_point_jeonbuk.json", 5),
|
||||
createRegionalLine("resource/Regional_point_jeonnam.json", 6),
|
||||
createRegionalLine("resource/Regional_point_daegu.json", 7),
|
||||
]);
|
||||
|
||||
const regionalSource = new ol.source.Vector({
|
||||
features: regional_lines,
|
||||
});
|
||||
|
||||
this.addVectorLayer({
|
||||
name: "Regional_Line",
|
||||
source: regionalSource,
|
||||
style: layerStyles.regional,
|
||||
zIndex: 0,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* WebGL Layer
|
||||
* author: khy
|
||||
*/
|
||||
const canvas = document.createElement("canvas");
|
||||
const gl = canvas?.getContext("webgl") || canvas?.getContext("experimental-webgl");
|
||||
const webglAvailable = gl instanceof WebGLRenderingContext;
|
||||
|
||||
class WebGLVectorLayer extends ol.layer.Layer {
|
||||
constructor(params) {
|
||||
super(params);
|
||||
this.style = params.style;
|
||||
}
|
||||
createRenderer() {
|
||||
const style = this.style;
|
||||
|
||||
return new ol.renderer.webgl.VectorLayer(this, {
|
||||
style,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function VectorLayer(params) {
|
||||
return new ol.layer.Vector(params);
|
||||
//return (webglAvailable) ? new WebGLVectorLayer(params) : new ol.layer.Vector(params);
|
||||
}
|
||||
|
||||
function PointsLayer(params) {
|
||||
const hasStyleFunction = typeof params?.style === "function";
|
||||
|
||||
return (webglAvailable && !hasStyleFunction) ? new ol.layer.WebGLPoints(params) : new ol.layer.Vector(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* ol.Map prototype 확장
|
||||
* @param ...args
|
||||
* @returns
|
||||
*/
|
||||
ol.Map.prototype.addLayerWithType = async function (...args) {
|
||||
const LayerType = args[0];
|
||||
args = args[1];
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
let url, fetchOption, layerOption;
|
||||
let response, geojson;
|
||||
|
||||
switch (args.length) {
|
||||
case 3:
|
||||
if (
|
||||
typeof args[0] === "string" && //fetch url
|
||||
typeof args[1] === "object" && //fetch option
|
||||
typeof args[2] === "object"
|
||||
) {
|
||||
//layer option
|
||||
(url = args[0]), (fetchOption = args[1]), (layerOption = args[2]);
|
||||
response = await fetch(url, fetchOption);
|
||||
geojson = await response.json();
|
||||
layerOption.source = await ol.source.Vector.fromGeoJSON(geojson);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
if (
|
||||
typeof args[0] === "string" && //fetch url
|
||||
typeof args[1] === "object"
|
||||
) {
|
||||
//layer option
|
||||
(url = args[0]), (layerOption = args[1]);
|
||||
|
||||
console.log(`addLayerWithType(${url})`, fetch);
|
||||
response = await fetch(url);
|
||||
geojson = await response.json();
|
||||
console.log(geojson);
|
||||
layerOption.source = await ol.source.Vector.fromGeoJSON(geojson);
|
||||
} else if (
|
||||
typeof args[0] === "object" && //layer option
|
||||
typeof args[1] === "object"
|
||||
) {
|
||||
//map object
|
||||
layerOption = args[0];
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
break;
|
||||
case 1: //layer option
|
||||
default:
|
||||
layerOption = args[0];
|
||||
}
|
||||
|
||||
if (layerOption.labelStyleFunction) {
|
||||
const layergroup = new ol.layer.Group({
|
||||
layers: [
|
||||
new LayerType({
|
||||
...layerOption,
|
||||
source: layerOption.source || new ol.source.Vector(),
|
||||
style: layerOption.style || ol.style.flat.createDefaultStyle(),
|
||||
}),
|
||||
new ol.layer.Vector({
|
||||
name: layerOption.name ? `${layerOption.name}_label` : "label",
|
||||
source: layerOption.source || new ol.source.Vector(),
|
||||
style: layerOption.labelStyleFunction,
|
||||
declutter: true,
|
||||
zIndex: layerOption.zIndex + 1,
|
||||
}),
|
||||
],
|
||||
});
|
||||
this.addLayer(layergroup);
|
||||
|
||||
resolve(layergroup);
|
||||
} else {
|
||||
const layer = new LayerType({
|
||||
...layerOption,
|
||||
source: layerOption.source || new ol.source.Vector(),
|
||||
style: layerOption.style || ol.style.flat.createDefaultStyle(),
|
||||
});
|
||||
|
||||
this.addLayer(layer);
|
||||
|
||||
resolve(layer);
|
||||
}
|
||||
} catch {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @author khy
|
||||
* @description WebGL벡터레이어나 일반벡터레이어를 생성하여 map객체에 addLayer()함
|
||||
* 전달인자를 1개, 2개, 3개를 줄 수 있음.
|
||||
* 전달인자 1개: layer option
|
||||
* 전달인자 2개: fetch url, layer option
|
||||
* 전달인자 3개: fetch url, fetch option, layer option
|
||||
* @param ...args
|
||||
* case (args0) Layer option
|
||||
* case (args0, args1) fetch url, layer option || layer option, map target
|
||||
* case (args0, args1, args2) fetch url, fetch option, layer option
|
||||
*/
|
||||
|
||||
/**
|
||||
* @returns
|
||||
* ol.layer.vector || WebGLVectorLayer
|
||||
*/
|
||||
ol.Map.prototype.addVectorLayer = async function (...args) {
|
||||
return new Promise(async (resolve) => {
|
||||
resolve(this.addLayerWithType(VectorLayer, args));
|
||||
});
|
||||
};
|
||||
ol.Map.prototype.addRailLayer = async function (...args) {
|
||||
return new Promise(async (resolve) => {
|
||||
resolve(this.addLayerWithType(ol.layer.Vector, args));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns
|
||||
* ol.layer.vector || ol.layer.WebGLPoints
|
||||
*/
|
||||
ol.Map.prototype.addPointsLayer = async function (...args) {
|
||||
return new Promise(async (resolve) => {
|
||||
resolve(this.addLayerWithType(PointsLayer, args));
|
||||
});
|
||||
};
|
||||
ol.Map.prototype.addNormalRailLayer = function (options) {
|
||||
return this.addVectorLayer(
|
||||
"resource/NormalLine.json",
|
||||
Object.assign(
|
||||
{
|
||||
name: "NormalLine",
|
||||
style: {
|
||||
"stroke-width": 2,
|
||||
},
|
||||
zIndex: 2,
|
||||
},
|
||||
options
|
||||
)
|
||||
);
|
||||
};
|
||||
ol.Map.prototype.addNormalStationLayer = function (options) {
|
||||
return this.addPointsLayer(
|
||||
"resource/NormalStation.json",
|
||||
Object.assign(
|
||||
{
|
||||
name: "NormalStation",
|
||||
style: {
|
||||
"circle-radius": 5,
|
||||
"circle-stroke-width": 3,
|
||||
"circle-fill-color": "gainsboro",
|
||||
},
|
||||
zIndex: 3,
|
||||
},
|
||||
options
|
||||
)
|
||||
);
|
||||
};
|
||||
ol.Map.prototype.addKTXRailLayer = function (options) {
|
||||
return this.addVectorLayer(
|
||||
"resource/KTXLine.json",
|
||||
Object.assign(
|
||||
{
|
||||
name: "KTXLine",
|
||||
style: {
|
||||
"stroke-color": "#005BAC",
|
||||
"stroke-width": 5,
|
||||
},
|
||||
zIndex: 1,
|
||||
},
|
||||
options
|
||||
)
|
||||
);
|
||||
};
|
||||
ol.Map.prototype.addKTXStationLayer = function (options) {
|
||||
return this.addPointsLayer(
|
||||
"resource/KTXStation.json",
|
||||
Object.assign(
|
||||
{
|
||||
name: "KTXStation",
|
||||
style: {
|
||||
"circle-radius": 7,
|
||||
"circle-stroke-color": "#005BAC",
|
||||
"circle-stroke-width": 3,
|
||||
"circle-fill-color": "gainsboro",
|
||||
},
|
||||
zIndex: 4,
|
||||
},
|
||||
options
|
||||
)
|
||||
);
|
||||
};
|
||||
ol.Map.prototype.addBaseRailLayer = function () {
|
||||
this.addNormalRailLayer();
|
||||
this.addKTXRailLayer();
|
||||
};
|
||||
ol.Map.prototype.addBaseStationLayer = function () {
|
||||
this.addNormalStationLayer();
|
||||
this.addKTXStationLayer();
|
||||
};
|
||||
|
||||
/*
|
||||
function removeLayer(layername) {
|
||||
getLayers(layername).forEach((layer) => {
|
||||
layer.dispose();
|
||||
map.removeLayer(layer);
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
ol.Map.prototype.getLayer = function (layername) {
|
||||
return this.getLayers(layername)[0];
|
||||
};
|
||||
ol.Map.prototype.getLayers = function (layername) {
|
||||
return layername ? this.getAllLayers().filter((layer) => layer.get("name") === layername) : this.getLayerGroup().getLayers();
|
||||
};
|
||||
ol.Map.prototype.flyTo = function (params) {
|
||||
this.once("postrender", () => {
|
||||
this.flyToNow(params);
|
||||
});
|
||||
};
|
||||
ol.Map.prototype.flyToNow = function (params) {
|
||||
const convertedCenter = params.center.toEPSG3857();
|
||||
|
||||
const option = Object.assign(params, {
|
||||
center: convertedCenter,
|
||||
duration: 700,
|
||||
easing: ol.easing.easeOut,
|
||||
});
|
||||
this.getView().animate(option);
|
||||
};
|
||||
ol.Map.prototype.setVisibleLayer = function (layername, visible) {
|
||||
this.getLayers(layername).forEach((layer) => {
|
||||
layer.setVisible(visible);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Overlay
|
||||
* author: khy
|
||||
* date: 23.09.07
|
||||
* */
|
||||
ol.Map.prototype.createOverlay = function () {
|
||||
if (!document.getElementById("popup")) {
|
||||
const popupElement = document.createElement("div");
|
||||
popupElement.id = "popup";
|
||||
this.getTargetElement().appendChild(popupElement);
|
||||
}
|
||||
|
||||
const popup = new ol.Overlay({
|
||||
id: "popup",
|
||||
element: document.getElementById("popup"),
|
||||
autoPan: true
|
||||
});
|
||||
|
||||
this.addOverlay(popup);
|
||||
|
||||
return popup;
|
||||
};
|
||||
ol.Map.prototype.getOverlay = function () {
|
||||
const popup = this.getOverlayById("popup") || this.createOverlay();
|
||||
|
||||
return popup;
|
||||
};
|
||||
ol.Map.prototype.showOverlay = function () {
|
||||
this.getOverlay().element.style.display = null;
|
||||
};
|
||||
ol.Map.prototype.hideOverlay = function () {
|
||||
this.getOverlay().element.style.display = "none";
|
||||
};
|
||||
ol.Map.prototype.isShowingOverlay = function () {
|
||||
return (map.getOverlay().element.style.display !== "none")
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag Interaction
|
||||
* author: khy
|
||||
* param
|
||||
* options : {
|
||||
* layerFilter: function(default=undefined),
|
||||
* hitTolerance: number(default=0),
|
||||
* checkWrapped: boolean(default=true),
|
||||
* }
|
||||
* usage
|
||||
* map.addInteraction(new Drag( options | null ));
|
||||
*
|
||||
*/
|
||||
class Drag extends ol.interaction.Pointer {
|
||||
constructor(options) {
|
||||
super({
|
||||
handleDownEvent: handleDownEvent,
|
||||
handleDragEvent: handleDragEvent,
|
||||
handleMoveEvent: handleMoveEvent,
|
||||
handleUpEvent: handleUpEvent,
|
||||
});
|
||||
|
||||
this.options_ = options;
|
||||
|
||||
this.coordinate_ = null;
|
||||
this.cursor_ = "grabbing";
|
||||
this.feature_ = null;
|
||||
this.previousCursor_ = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDownEvent(evt) {
|
||||
const map = evt.map;
|
||||
const feature = map.forEachFeatureAtPixel(evt.pixel, (feature) => feature, this.options_);
|
||||
|
||||
if (feature) {
|
||||
this.coordinate_ = evt.coordinate;
|
||||
this.feature_ = feature;
|
||||
}
|
||||
|
||||
return !!feature;
|
||||
}
|
||||
function handleDragEvent(evt) {
|
||||
const deltaX = evt.coordinate[0] - this.coordinate_[0];
|
||||
const deltaY = evt.coordinate[1] - this.coordinate_[1];
|
||||
|
||||
const geometry = this.feature_.getGeometry();
|
||||
geometry.translate(deltaX, deltaY);
|
||||
|
||||
this.coordinate_[0] = evt.coordinate[0];
|
||||
this.coordinate_[1] = evt.coordinate[1];
|
||||
}
|
||||
function handleMoveEvent(evt) {
|
||||
if (this.cursor_) {
|
||||
const map = evt.map;
|
||||
const feature = map.forEachFeatureAtPixel(evt.pixel, (feature) => feature, this.options_);
|
||||
|
||||
const element = evt.map.getTargetElement();
|
||||
|
||||
if (feature) {
|
||||
if (element.style.cursor != this.cursor_) {
|
||||
this.previousCursor_ = element.style.cursor;
|
||||
element.style.cursor = this.cursor_;
|
||||
}
|
||||
} else if (this.previousCursor_ !== undefined) {
|
||||
element.style.cursor = this.previousCursor_;
|
||||
this.previousCursor_ = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
function handleUpEvent() {
|
||||
this.coordinate_ = null;
|
||||
this.feature_ = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* loading spinner
|
||||
* author: khy
|
||||
* start
|
||||
* mapLoadStart();
|
||||
* end
|
||||
* mapLoadEnd();
|
||||
*/
|
||||
ol.Map.prototype.addLoadingEffect = function (option) {
|
||||
const css = `
|
||||
@keyframes spinner {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
#map {
|
||||
background: linear-gradient(to top left, #005bac, #3a3a3f);
|
||||
}
|
||||
${option?.transparent ? "/*" : ""}
|
||||
#map > .ol-viewport {
|
||||
display: none;
|
||||
}
|
||||
${option?.transparent ? "*/" : ""}
|
||||
${option?.background ? "" : "/*"}
|
||||
#map.spinner::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: ${option?.background};
|
||||
z-index:8;
|
||||
}
|
||||
${option?.background ? "" : "*/"}
|
||||
#map.spinner::after {
|
||||
content: "";
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-top: -20px;
|
||||
margin-left: -20px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid ${option?.color ? option.color : "white"};
|
||||
border-top-color: transparent;
|
||||
animation: spinner 0.6s linear infinite;
|
||||
z-index:9;
|
||||
}
|
||||
`;
|
||||
|
||||
const head = document.head || document.getElementsByTagName("head")[0];
|
||||
|
||||
if (head.querySelector("style[usage=spinner]")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.setAttribute("usage", "spinner");
|
||||
style.type = "text/css";
|
||||
|
||||
if (style.styleSheet) {
|
||||
style.styleSheet.cssText = css;
|
||||
} else {
|
||||
style.appendChild(document.createTextNode(css));
|
||||
}
|
||||
|
||||
head.appendChild(style);
|
||||
};
|
||||
ol.Map.prototype.removeLoadingEffect = function () {
|
||||
const head = document.head || document.getElementsByTagName("head")[0];
|
||||
head.querySelector("style[usage=spinner]")?.remove();
|
||||
};
|
||||
ol.Map.prototype.startLoadingEffect = function (option) {
|
||||
this.addLoadingEffect(option);
|
||||
this.getTargetElement().classList.add("spinner");
|
||||
};
|
||||
ol.Map.prototype.finishLoadingEffect = function () {
|
||||
this.removeLoadingEffect();
|
||||
this.getTargetElement().classList.remove("spinner");
|
||||
};
|
||||
350
static/ol.css
Normal file
350
static/ol.css
Normal file
@@ -0,0 +1,350 @@
|
||||
:root,
|
||||
:host {
|
||||
--ol-background-color: white;
|
||||
--ol-accent-background-color: #F5F5F5;
|
||||
--ol-subtle-background-color: rgba(128, 128, 128, 0.25);
|
||||
--ol-partial-background-color: rgba(255, 255, 255, 0.75);
|
||||
--ol-foreground-color: #333333;
|
||||
--ol-subtle-foreground-color: #666666;
|
||||
--ol-brand-color: #00AAFF;
|
||||
}
|
||||
|
||||
.ol-box {
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
border: 1.5px solid var(--ol-background-color);
|
||||
background-color: var(--ol-partial-background-color);
|
||||
}
|
||||
|
||||
.ol-mouse-position {
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ol-scale-line {
|
||||
background: var(--ol-partial-background-color);
|
||||
border-radius: 4px;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
padding: 2px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ol-scale-line-inner {
|
||||
border: 1px solid var(--ol-subtle-foreground-color);
|
||||
border-top: none;
|
||||
color: var(--ol-foreground-color);
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
margin: 1px;
|
||||
will-change: contents, width;
|
||||
transition: all 0.25s;
|
||||
}
|
||||
|
||||
.ol-scale-bar {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
.ol-scale-bar-inner {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ol-scale-step-marker {
|
||||
width: 1px;
|
||||
height: 15px;
|
||||
background-color: var(--ol-foreground-color);
|
||||
float: right;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.ol-scale-step-text {
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
font-size: 10px;
|
||||
z-index: 11;
|
||||
color: var(--ol-foreground-color);
|
||||
text-shadow: -1.5px 0 var(--ol-partial-background-color), 0 1.5px var(--ol-partial-background-color), 1.5px 0 var(--ol-partial-background-color), 0 -1.5px var(--ol-partial-background-color);
|
||||
}
|
||||
|
||||
.ol-scale-text {
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
bottom: 25px;
|
||||
color: var(--ol-foreground-color);
|
||||
text-shadow: -1.5px 0 var(--ol-partial-background-color), 0 1.5px var(--ol-partial-background-color), 1.5px 0 var(--ol-partial-background-color), 0 -1.5px var(--ol-partial-background-color);
|
||||
}
|
||||
|
||||
.ol-scale-singlebar {
|
||||
position: relative;
|
||||
height: 10px;
|
||||
z-index: 9;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--ol-foreground-color);
|
||||
}
|
||||
|
||||
.ol-scale-singlebar-even {
|
||||
background-color: var(--ol-subtle-foreground-color);
|
||||
}
|
||||
|
||||
.ol-scale-singlebar-odd {
|
||||
background-color: var(--ol-background-color);
|
||||
}
|
||||
|
||||
.ol-unsupported {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ol-viewport,
|
||||
.ol-unselectable {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.ol-viewport canvas {
|
||||
all: unset;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ol-viewport {
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
|
||||
.ol-selectable {
|
||||
-webkit-touch-callout: default;
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.ol-grabbing {
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.ol-grab {
|
||||
cursor: move;
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.ol-control {
|
||||
position: absolute;
|
||||
background-color: var(--ol-subtle-background-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ol-zoom {
|
||||
top: .5em;
|
||||
left: .5em;
|
||||
}
|
||||
|
||||
.ol-rotate {
|
||||
top: .5em;
|
||||
right: .5em;
|
||||
transition: opacity .25s linear, visibility 0s linear;
|
||||
}
|
||||
|
||||
.ol-rotate.ol-hidden {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity .25s linear, visibility 0s linear .25s;
|
||||
}
|
||||
|
||||
.ol-zoom-extent {
|
||||
top: 4.643em;
|
||||
left: .5em;
|
||||
}
|
||||
|
||||
.ol-full-screen {
|
||||
right: .5em;
|
||||
top: .5em;
|
||||
}
|
||||
|
||||
.ol-control button {
|
||||
display: block;
|
||||
margin: 1px;
|
||||
padding: 0;
|
||||
color: var(--ol-subtle-foreground-color);
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
text-align: center;
|
||||
height: 1.375em;
|
||||
width: 1.375em;
|
||||
line-height: .4em;
|
||||
background-color: var(--ol-background-color);
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.ol-control button::-moz-focus-inner {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ol-zoom-extent button {
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
.ol-compass {
|
||||
display: block;
|
||||
font-weight: normal;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.ol-touch .ol-control button {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.ol-touch .ol-zoom-extent {
|
||||
top: 5.5em;
|
||||
}
|
||||
|
||||
.ol-control button:hover,
|
||||
.ol-control button:focus {
|
||||
text-decoration: none;
|
||||
outline: 1px solid var(--ol-subtle-foreground-color);
|
||||
color: var(--ol-foreground-color);
|
||||
}
|
||||
|
||||
.ol-zoom .ol-zoom-in {
|
||||
border-radius: 2px 2px 0 0;
|
||||
}
|
||||
|
||||
.ol-zoom .ol-zoom-out {
|
||||
border-radius: 0 0 2px 2px;
|
||||
}
|
||||
|
||||
.ol-attribution {
|
||||
text-align: right;
|
||||
bottom: .5em;
|
||||
right: .5em;
|
||||
max-width: calc(100% - 1.3em);
|
||||
display: flex;
|
||||
flex-flow: row-reverse;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ol-attribution a {
|
||||
color: var(--ol-subtle-foreground-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.ol-attribution ul {
|
||||
margin: 0;
|
||||
padding: 1px .5em;
|
||||
color: var(--ol-foreground-color);
|
||||
text-shadow: 0 0 2px var(--ol-background-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ol-attribution li {
|
||||
display: inline;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.ol-attribution li:not(:last-child):after {
|
||||
content: " ";
|
||||
}
|
||||
|
||||
.ol-attribution img {
|
||||
max-height: 2em;
|
||||
max-width: inherit;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ol-attribution button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ol-attribution.ol-collapsed ul {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ol-attribution:not(.ol-collapsed) {
|
||||
background: var(--ol-partial-background-color);
|
||||
}
|
||||
|
||||
.ol-attribution.ol-uncollapsible {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
border-radius: 4px 0 0;
|
||||
}
|
||||
|
||||
.ol-attribution.ol-uncollapsible img {
|
||||
margin-top: -.2em;
|
||||
max-height: 1.6em;
|
||||
}
|
||||
|
||||
.ol-attribution.ol-uncollapsible button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ol-zoomslider {
|
||||
top: 4.5em;
|
||||
left: .5em;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.ol-zoomslider button {
|
||||
position: relative;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.ol-touch .ol-zoomslider {
|
||||
top: 5.5em;
|
||||
}
|
||||
|
||||
.ol-overviewmap {
|
||||
left: 0.5em;
|
||||
bottom: 0.5em;
|
||||
}
|
||||
|
||||
.ol-overviewmap.ol-uncollapsible {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
border-radius: 0 4px 0 0;
|
||||
}
|
||||
|
||||
.ol-overviewmap .ol-overviewmap-map,
|
||||
.ol-overviewmap button {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ol-overviewmap .ol-overviewmap-map {
|
||||
border: 1px solid var(--ol-subtle-foreground-color);
|
||||
height: 150px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.ol-overviewmap:not(.ol-collapsed) button {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ol-overviewmap.ol-collapsed .ol-overviewmap-map,
|
||||
.ol-overviewmap.ol-uncollapsible button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ol-overviewmap:not(.ol-collapsed) {
|
||||
background: var(--ol-subtle-background-color);
|
||||
}
|
||||
|
||||
.ol-overviewmap-box {
|
||||
border: 1.5px dotted var(--ol-subtle-foreground-color);
|
||||
}
|
||||
|
||||
.ol-overviewmap .ol-overviewmap-box:hover {
|
||||
cursor: move;
|
||||
}
|
||||
4
static/ol.js
Normal file
4
static/ol.js
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user