KorailMap/static/main.js

662 lines
22 KiB
JavaScript

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;
}
}