This commit is contained in:
Morgan 2024-09-10 10:27:25 +09:00
commit fb145c44f2
No known key found for this signature in database
15 changed files with 22873 additions and 0 deletions

289
index.html Normal file
View File

@ -0,0 +1,289 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Korail GIS</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
#map {
height: calc(100vh - 60px);
background-color: #e5e5e5;
}
.station-label {
text-align: center;
font-weight: bold;
font-size: 15px;
color: black;
}
.train-marker {
text-align: center;
}
.bottombar {
height: 40px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
}
.bottombar .left {
display: flex;
align-items: center;
}
.bottombar .left input[type="checkbox"] {
margin-right: 10px;
}
.bottombar .right {
display: flex;
align-items: center;
}
.bottombar .loading {
margin-right: 15px;
}
#reloadTrains {
padding: 5px 10px;
background-color: #007BFF;
color: white;
border: none;
cursor: pointer;
border-radius: 4px;
}
#reloadTrains:hover {
background-color: #0056b3;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div id="map"></div>
<div class="bottombar">
<div class="left">
<input type="checkbox" id="layer0"> Express
<input type="checkbox" id="layer1"> Semi
<input type="checkbox" id="layer2"> Normal
<input type="checkbox" id="layer3"> Passenger
<input type="checkbox" id="layer4"> Subway
<input type="checkbox" id="layer5"> Logis
</div>
<div class="right">
<span class="loading" id="loading">Loading...</span>
<button id="reloadTrains">Reload</button>
<input type="checkbox" id="autoReload"> Auto
</div>
</div>
<script>
async function myFetch(url, options = {}) {
let headers = options.headers
let f = fetch('https://proxy.devpg.net', {
headers: {
'X-Proxy-URL': url,
'X-Proxy-Header': JSON.stringify(headers),
'X-Proxy-Cache': '0'
}
});
console.log(`fetch(${url}, headers=${JSON.stringify(headers)}`, f);
return f
}
const trainAPI = 'https://gis.korail.com/api/train?bbox=120.6263671875,28.07910949377748,134.0736328125,45.094739803960664'
const server = ''
const map = L.map('map').setView([36.5, 128], 7.5);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
const railLayers = {
express: L.layerGroup().addTo(map),
normal: L.layerGroup().addTo(map),
semi: L.layerGroup().addTo(map),
logis: L.layerGroup().addTo(map)
};
const stationLayers = [];
for (let i = 0; i < 64; i++) {
const layerGroup = L.layerGroup().addTo(map);
stationLayers.push(layerGroup);
}
for (let layer of stationLayers) {
map.removeLayer(layer);
}
const trainLayer = L.layerGroup().addTo(map);
function startSpinner() {
document.getElementById("loading").classList.remove("hidden");
}
function clearSpinner() {
document.getElementById("loading").classList.add("hidden");
}
function loadRail(type) {
fetch(`${server}/api/rail/${type}`)
.then(response => {
if (!response.ok) {
throw new Error(`Network response was not ok ${response.statusText}`);
}
return response.json();
})
.then(data => {
L.geoJSON(data, {
style: {
color: type === 'express' ? 'blue' : type === 'normal' ? 'green' : type === 'semi' ? 'orange' : 'red'
}
}).addTo(railLayers[type]);
})
.catch(error => console.error(`Error fetching ${type} rail data: `, error));
}
function loadStations() {
fetch(`${server}/api/station`)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
L.geoJSON(data, {
pointToLayer: function (feature, latlng) {
const shownLayer = parseInt(feature.properties.shown_layer.slice(2), 2);
const marker = L.marker(latlng, {
icon: L.divIcon({
className: 'station-label',
html: `<div>${feature.properties.name}</div>`,
iconSize: [100, 0]
})
});
if (stationLayers[shownLayer]) {
stationLayers[shownLayer].addLayer(marker);
}
return marker;
}
});
})
.catch(error => console.error('Error fetching station data: ', error));
}
function loadTrains() {
startSpinner();
myFetch(trainAPI, {
headers: {
"x-requested-with": "com.korail.talk",
"referer": "https://gis.korail.com/korailTalk/entrance",
"user-agent": "korailtalk AppVersion/6.3.3"
}
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
trainLayer.clearLayers();
L.geoJSON(data, {
pointToLayer: function (feature, latlng) {
const trainIconHtml = `
<div style="position: relative; text-align: center;">
<div style="background-color: #007bff;
border-radius: 50%;
width: 16px;
height: 16px;
display: inline-block;
border: 2px solid white;"></div>
<div style="font-size: 12px;
color: black;
background-color: white;
border: 1px solid black;
margin-top: 2px;
padding: 1px 3px;
border-radius: 3px;
display: inline-block;
position: absolute;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;">${feature.properties.trn_case} ${feature.properties.trn_no}</div>
</div>`;
return L.marker(latlng, {
icon: L.divIcon({
className: 'train-marker',
html: trainIconHtml,
iconSize: [30, 42],
iconAnchor: [15, 21]
})
}).bindPopup(`${feature.properties.trn_case} ${feature.properties.trn_no}<br>
(${feature.properties.dpt_stn_nm} -> ${feature.properties.arv_stn_nm})<br>
${feature.properties.now_stn} ${feature.properties.next_stn}`);
}
}).addTo(trainLayer);
clearSpinner();
})
.catch(error => console.error('Error fetching train data: ', error));
}
loadRail('express');
loadRail('normal');
loadRail('semi');
loadRail('logis');
for (let f = 0; f < 6; f++) {
document.getElementById(`layer${f}`).addEventListener('change', function () { // lable0: 0b 1XXXXX, label1: 0bX1XXXX, ...
for (let i = 0; i < 64; i++) {
if ( i & (1 << (5 - 0)) ) {
if (this.checked) {
map.addLayer(stationLayers[i]);
} else {
map.removeLayer(stationLayers[i]);
}
}
}
})
}
document.getElementById('reloadTrains').addEventListener('click', loadTrains);
document.getElementById('autoReload').addEventListener('click', autoReload);
let reloadInterval = null
function autoReload() {
if (this.checked) {
reloadInterval = setInterval(loadTrains, 2000);
}
else {
if (reloadInterval) {
clearInterval(reloadInterval);
reloadInterval = null;
}
}
}
loadStations();
loadTrains();
</script>
</body>
</html>

1
json/rail_express.json Normal file

File diff suppressed because one or more lines are too long

1
json/rail_logis.json Normal file

File diff suppressed because one or more lines are too long

1
json/rail_normal.json Normal file

File diff suppressed because one or more lines are too long

1
json/rail_semi.json Normal file

File diff suppressed because one or more lines are too long

15544
json/station.json Normal file

File diff suppressed because it is too large Load Diff

1
json/station.old.json Normal file

File diff suppressed because one or more lines are too long

5025
json/trains.json Normal file

File diff suppressed because it is too large Load Diff

50
main.py Normal file
View File

@ -0,0 +1,50 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi import Request, Response
from fastapi.responses import JSONResponse, FileResponse
import json
import requests
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
async def root():
return FileResponse("index.html")
@app.get("/api/station")
async def getApiStation():
with open("json/station.json", "r") as f:
return json.load(f)
@app.get("/api/rail/{type}")
async def getApiRail(type: str):
with open(f"json/rail_{type}.json", "r") as f:
return json.load(f)
@app.get("/api/train")
async def getApiTrain():
tapi = requests.get('https://gis.korail.com/api/train?bbox=120.6263671875,28.07910949377748,134.0736328125,45.094739803960664',
headers = {
"x-requested-with": "com.korail.talk",
"referer": "https://gis.korail.com/korailTalk/entrance",
"user-agent": "korailtalk AppVersion/6.3.3"
})
return tapi.json()
with open("json/trains.json", "r") as f:
return json.load(f)
if __name__=="__main__":
import uvicorn
uvicorn.run(app)

30
static/gis.html Normal file
View 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
View 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
View 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
View 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
View 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

File diff suppressed because one or more lines are too long