feat(dashboard): CMU's 2024 flood marks as a map layer
CMU Water Center marked the October 2024 high-water line at ~4,900 places in Chiang Mai and Lamphun (depth above the road while P.1 stood at 5.30 m). - src/static/flood-marks-2024.json (position and depth, 110 KB), built by scripts/build_flood_marks.py from CMU's GeoJSON. - "Flood marks 2024" layer: canvas dots from zoom 12 in CMU's five depth classes, legend row, CMU credit; turning it on below zoom 12 flies to the city. A mark's popup gives the depth and its flood zone. - Zone and marker popups name the nearest mark within 250 m (loaded with the zones). - No depth estimate for other P.1 levels: the marks do not scale one-to-one with P.1 (marks in the 5.30 m zone stood a median 65 cm deep in 2024). - The layer bar wraps on narrow maps; popups pan clear of it. - Docs: DATA_SOURCES 5.1, DASHBOARD_REDESIGN, README.
This commit is contained in:
1 parent
701bb87d34
commit
a610abcdb8
6 files changed
+191
-10
No files matched your search
+105
-3
@@ -188,6 +188,7 @@
|
||||
.st-label::before { display: none; }
|
||||
[data-theme="dark"] .st-label { color: #e6eaf0; text-shadow: 0 0 3px #000, 0 0 3px #000, 0 0 3px #000; }
|
||||
.layers { position: absolute; z-index: 800; right: 12px; top: 12px; display: flex; gap: 2px; padding: 3px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); box-shadow: var(--shadow); }
|
||||
.layers { flex-wrap: wrap; justify-content: flex-end; max-width: calc(100% - 64px); }
|
||||
.layers button { padding: 5px 10px; border-radius: 5px; font-size: 12px; font-weight: 500; color: var(--muted); display: inline-flex; align-items: center; gap: 6px; }
|
||||
.layers button::before { content: ""; width: 12px; height: 12px; border-radius: 3px; border: 1.5px solid var(--line-2); }
|
||||
.layers button[aria-pressed="true"] { color: var(--text); }
|
||||
@@ -404,6 +405,7 @@
|
||||
<button type="button" id="layer-basin" aria-pressed="false" data-i18n="layer.basin">Basin stations</button>
|
||||
<button type="button" id="layer-rain" aria-pressed="false" data-i18n="layer.rain">Rain 24 h</button>
|
||||
<button type="button" id="layer-zones" aria-pressed="false" data-i18n="layer.zones">Flood zones</button>
|
||||
<button type="button" id="layer-marks" aria-pressed="false" data-i18n="layer.marks">Flood marks 2024</button>
|
||||
</div>
|
||||
<div class="legend" id="legend">
|
||||
<div>
|
||||
@@ -431,6 +433,15 @@
|
||||
<span class="key"><i style="background:#f5d547"></i><span data-i18n="legend.zones.last">Zone 5 last</span></span>
|
||||
<span class="key"><i style="background:#2f86c8"></i><span data-i18n="legend.zones.flooded">River at or above the zone's level</span></span>
|
||||
</div>
|
||||
<div id="legend-marks" hidden>
|
||||
<span class="label" data-i18n="layer.marks">Flood marks 2024</span>
|
||||
<span class="key"><span data-i18n="legend.marks.unit">Depth above the road, cm:</span></span>
|
||||
<span class="key"><i class="dot" style="background:#2e9e4f"></i><span class="num">0–40</span></span>
|
||||
<span class="key"><i class="dot" style="background:#e6c229"></i><span class="num">41–80</span></span>
|
||||
<span class="key"><i class="dot" style="background:#f08a24"></i><span class="num">81–120</span></span>
|
||||
<span class="key"><i class="dot" style="background:#d7301f"></i><span class="num">121–160</span></span>
|
||||
<span class="key"><i class="dot" style="background:#7b3294"></i><span class="num">> 160</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay" id="error"><div><strong id="error-message"></strong><br><button type="button" class="btn" id="retry" data-i18n="error.retry">Retry now</button></div></div>
|
||||
</section>
|
||||
@@ -482,7 +493,7 @@
|
||||
'use strict';
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const state = { lang: 'th', stations: [], byCode: new Map(), selected: null, sort: 'river', map: null, tiles: null, markers: new Map(),
|
||||
rivers: null, riverLayer: null, rain: [], rainLayer: null, zones: null, zoneLayer: null, zonesUserHidden: false, fitted: false,
|
||||
rivers: null, riverLayer: null, rain: [], rainLayer: null, zones: null, zoneLayer: null, zonesUserHidden: false, marks: null, markLayer: null, fitted: false,
|
||||
p1Stages: null, alerts: null, lastLoad: null, tab: 'rid', bsort: 'bank', basin: [], basinById: new Map(), basinOn: false,
|
||||
basinLayer: null, basinMarkers: new Map(), pois: null, poiLayer: null,
|
||||
mode: 'live', live: [], replay: null, replayData: null, simLevel: null, simRise: false, simTimer: null,
|
||||
@@ -551,6 +562,15 @@
|
||||
'layer.group': 'Map layers',
|
||||
'layer.rain': 'Rain 24 h',
|
||||
'layer.zones': 'Flood zones',
|
||||
'layer.marks': 'Flood marks 2024',
|
||||
'legend.marks.unit': 'Depth above the road, cm:',
|
||||
'mark.title': 'Flood mark, October 2024',
|
||||
'mark.depth': (cm) => `${cm} cm above the road`,
|
||||
'mark.p1': 'The high-water line of the 2024 flood, when P.1 stood at 5.30 m (the record).',
|
||||
'mark.nozone': 'Outside the mapped zones',
|
||||
'mark.near': 'Flood mark 2024 nearby',
|
||||
'mark.near.value': (cm, m) => `${cm} cm, ${m} m away`,
|
||||
'mark.source': 'Surveyed by CMU Water Center',
|
||||
'map.aria': 'Map of the Ping River gauges',
|
||||
'detail.aria': 'Selected gauge',
|
||||
'error.load': (msg) => `The data could not be loaded (${msg}). The page retries every 5 minutes.`,
|
||||
@@ -810,6 +830,15 @@
|
||||
'layer.group': 'ชั้นข้อมูลแผนที่',
|
||||
'layer.rain': 'ฝน 24 ชม.',
|
||||
'layer.zones': 'พื้นที่น้ำท่วม',
|
||||
'layer.marks': 'คราบน้ำท่วม 2567',
|
||||
'legend.marks.unit': 'ความสูงน้ำจากผิวถนน (ซม.):',
|
||||
'mark.title': 'เครื่องหมายระดับน้ำท่วม ตุลาคม 2567',
|
||||
'mark.depth': (cm) => `สูงจากผิวถนน ${cm} ซม.`,
|
||||
'mark.p1': 'ระดับน้ำท่วมสูงสุดปี 2567 ขณะที่ P.1 อยู่ที่ 5.30 ม. (สูงสุดเป็นประวัติการณ์)',
|
||||
'mark.nozone': 'อยู่นอกพื้นที่ที่ทำแผนที่ไว้',
|
||||
'mark.near': 'คราบน้ำท่วม 2567 ใกล้เคียง',
|
||||
'mark.near.value': (cm, m) => `${cm} ซม. ห่าง ${m} ม.`,
|
||||
'mark.source': 'สำรวจโดย ศูนย์วิชาการสนับสนุนการบริหารจัดการน้ำ มช.',
|
||||
'map.aria': 'แผนที่สถานีวัดน้ำแม่น้ำปิง',
|
||||
'detail.aria': 'สถานีที่เลือก',
|
||||
'error.load': (msg) => `โหลดข้อมูลไม่สำเร็จ (${msg}) หน้านี้จะลองใหม่ทุก 5 นาที`,
|
||||
@@ -1538,17 +1567,21 @@
|
||||
// ---- Map ------------------------------------------------------------
|
||||
function initMap() {
|
||||
if (!window.L) throw new Error(t('error.leaflet'));
|
||||
// pan popups clear of the layer bar, which wraps to two rows on phones
|
||||
L.Popup.mergeOptions({ autoPanPaddingTopLeft: L.point(12, 100) });
|
||||
const map = L.map('map', { zoomControl: true, attributionControl: true, minZoom: 8, maxZoom: 16,
|
||||
maxBounds: [[16.3, 97.2], [20.7, 100.6]], maxBoundsViscosity: 0.7 }).setView([18.75, 98.95], 9);
|
||||
map.createPane('zones').style.zIndex = 380;
|
||||
map.createPane('rivers').style.zIndex = 390;
|
||||
map.createPane('rain').style.zIndex = 420;
|
||||
map.createPane('marks').style.zIndex = 430;
|
||||
map.createPane('basin').style.zIndex = 440;
|
||||
map.createPane('pois').style.zIndex = 445;
|
||||
map.createPane('gauges').style.zIndex = 450;
|
||||
state.map = map;
|
||||
setBasemap();
|
||||
map.on('zoomend', syncPois);
|
||||
map.on('zoomend', syncMarks);
|
||||
}
|
||||
|
||||
// The basemap is a Protomaps extract of the Ping basin served by this
|
||||
@@ -1723,6 +1756,7 @@
|
||||
body = `<div class="pgrid"><div><span>${escapeHtml(t('poi.zone'))}</span><b>${escapeHtml(t('poi.zone.value', zone.properties.zone, Number(zone.properties.level).toFixed(2)))}</b></div>
|
||||
<div><span>${escapeHtml(t('zone.chance24'))}</span><b>${st ? pct(st.p_exceed) : '–'}</b></div></div>`;
|
||||
}
|
||||
body += nearMarkHtml(poi.lat, poi.lon);
|
||||
return `<div class="popup"><div class="label">${escapeHtml(t('poi.label'))}</div><h3>${escapeHtml(poi.name)}</h3>${poi.note ? `<div style="color:var(--muted)">${escapeHtml(poi.note)}</div>` : ''}${body}</div>`;
|
||||
}
|
||||
|
||||
@@ -1742,11 +1776,12 @@
|
||||
const st = zoneStage(zone);
|
||||
return { pane: 'zones', color: '#8a5a2b', weight: 1.1, dashArray: '4 4', fillColor: ZONE_COLORS[zone] || '#f5d547', fillOpacity: 0.34 + 0.3 * Math.min(1, st ? st.p_exceed : 0) };
|
||||
}
|
||||
function zonePopup(feature) {
|
||||
function zonePopup(feature, at) {
|
||||
const zone = feature.properties.zone, level = Number(feature.properties.level);
|
||||
const st = zoneStage(zone), now = p1Level();
|
||||
return `<div class="popup"><div class="label">${escapeHtml(t('zone.title', zone))}</div><h3>${escapeHtml(t('zone.floods', level.toFixed(2)))}</h3>
|
||||
<div class="pgrid"><div><span>${escapeHtml(t('zone.p1now'))}</span><b>${m2(now)} m</b></div><div><span>${escapeHtml(t('zone.chance24'))}</span><b>${st ? pct(st.p_exceed) : '–'}</b></div></div>
|
||||
${at ? nearMarkHtml(at.lat, at.lng) : ''}
|
||||
${now != null && now >= level ? `<div class="pfoot" style="color:var(--danger-ink);font-weight:600">${escapeHtml(t('zone.reached'))}</div>` : ''}</div>`;
|
||||
}
|
||||
async function setZones(on, fly) {
|
||||
@@ -1757,12 +1792,78 @@
|
||||
if (on) {
|
||||
try {
|
||||
if (!state.zones) state.zones = await json(fetch('/static/flood-zones-p1.geojson'));
|
||||
state.zoneLayer = L.geoJSON(state.zones, { style: zoneStyle, pane: 'zones', attribution: ZONE_SOURCE }).bindPopup((layer) => zonePopup(layer.feature)).addTo(state.map);
|
||||
state.zoneLayer = L.geoJSON(state.zones, { style: zoneStyle, pane: 'zones', attribution: ZONE_SOURCE })
|
||||
.bindPopup((layer) => zonePopup(layer.feature, state.zoneLayer && state.zoneLayer.getPopup() ? state.zoneLayer.getPopup().getLatLng() : null)).addTo(state.map);
|
||||
loadMarks(); // for the nearest flood mark in the zone popups
|
||||
if (fly) state.map.flyToBounds(state.zoneLayer.getBounds(), { maxZoom: 13, duration: 0.7 });
|
||||
} catch (e) { $('layer-zones').setAttribute('aria-pressed', 'false'); $('legend-zones').hidden = true; }
|
||||
}
|
||||
renderDetail();
|
||||
}
|
||||
// CMU Water Center's flood marks of October 2024 (scripts/build_flood_marks.py):
|
||||
// the high-water line above the road at ~4,900 places while P.1 stood at
|
||||
// 5.30 m, in CMU's depth classes. Drawn from zoom 12 (below that they are
|
||||
// one blob); a 2024 fact, so also shown in the replay.
|
||||
const MARK_BINS = [[40, '#2e9e4f'], [80, '#e6c229'], [120, '#f08a24'], [160, '#d7301f'], [Infinity, '#7b3294']];
|
||||
const markColor = (cm) => MARK_BINS.find(([max]) => cm <= max)[1];
|
||||
const MARK_SOURCE = '<a href="https://watercenter.cmu.ac.th/chiangmai/cmflood/warning/floodmark" target="_blank" rel="noopener">CMU Water Center</a>';
|
||||
async function loadMarks() {
|
||||
if (!state.marks) state.marks = await json(fetch('/static/flood-marks-2024.json')).then((d) => (Array.isArray(d.marks) ? d.marks : null)).catch(() => null);
|
||||
return state.marks;
|
||||
}
|
||||
// the closest mark within maxM metres (marks are [lon, lat, cm])
|
||||
function nearestMark(lat, lon, maxM) {
|
||||
if (!state.marks) return null;
|
||||
const kx = 111320 * Math.cos((lat * Math.PI) / 180), ky = 110540;
|
||||
let best = null, bestD = maxM;
|
||||
state.marks.forEach((m) => {
|
||||
const d = Math.hypot((m[0] - lon) * kx, (m[1] - lat) * ky);
|
||||
if (d < bestD) { bestD = d; best = m; }
|
||||
});
|
||||
return best ? { cm: best[2], m: bestD } : null;
|
||||
}
|
||||
function nearMarkHtml(lat, lon) {
|
||||
const near = nearestMark(lat, lon, 250);
|
||||
if (!near) return '';
|
||||
return `<div class="pgrid"><div><span>${escapeHtml(t('mark.near'))}</span><b>${escapeHtml(t('mark.near.value', near.cm, Math.max(10, Math.round(near.m / 10) * 10)))}</b></div></div>`;
|
||||
}
|
||||
function markPopup(m) {
|
||||
const zone = zoneOfPoint(m[1], m[0]);
|
||||
const where = zone ? t('poi.zone.value', zone.properties.zone, Number(zone.properties.level).toFixed(2)) : t('mark.nozone');
|
||||
return `<div class="popup"><div class="label">${escapeHtml(t('mark.title'))}</div><h3>${escapeHtml(t('mark.depth', m[2]))}</h3>
|
||||
<div style="color:var(--muted)">${escapeHtml(t('mark.p1'))}</div>
|
||||
<div class="pgrid"><div><span>${escapeHtml(t('poi.zone'))}</span><b>${escapeHtml(where)}</b></div></div>
|
||||
<div class="pfoot">${escapeHtml(t('mark.source'))}</div></div>`;
|
||||
}
|
||||
async function setMarks(on) {
|
||||
if (!state.map) return;
|
||||
if (state.markLayer) { if (state.map.hasLayer(state.markLayer)) state.map.removeLayer(state.markLayer); state.markLayer = null; }
|
||||
$('layer-marks').setAttribute('aria-pressed', String(on));
|
||||
$('legend-marks').hidden = !on;
|
||||
if (!on) return;
|
||||
const marks = await loadMarks();
|
||||
if (!state.zones) state.zones = await json(fetch('/static/flood-zones-p1.geojson')).catch(() => null);
|
||||
if (!marks || $('layer-marks').getAttribute('aria-pressed') !== 'true') {
|
||||
$('layer-marks').setAttribute('aria-pressed', 'false');
|
||||
$('legend-marks').hidden = true;
|
||||
return;
|
||||
}
|
||||
const renderer = L.canvas({ pane: 'marks' });
|
||||
const group = L.layerGroup([], { attribution: MARK_SOURCE });
|
||||
marks.forEach((m) => {
|
||||
L.circleMarker([m[1], m[0]], { renderer, pane: 'marks', radius: 3.5, weight: 0.6, color: 'rgba(0,0,0,0.45)', fillColor: markColor(m[2]), fillOpacity: 0.9 })
|
||||
.bindPopup(() => markPopup(m)).addTo(group);
|
||||
});
|
||||
state.markLayer = group;
|
||||
if (state.map.getZoom() < 12) state.map.flyTo([18.765, 99.0], 13, { duration: 0.7 });
|
||||
syncMarks();
|
||||
}
|
||||
function syncMarks() {
|
||||
if (!state.markLayer) return;
|
||||
const show = state.map.getZoom() >= 12;
|
||||
if (show && !state.map.hasLayer(state.markLayer)) state.markLayer.addTo(state.map);
|
||||
if (!show && state.map.hasLayer(state.markLayer)) state.map.removeLayer(state.markLayer);
|
||||
}
|
||||
function autoZones() {
|
||||
// The river over its banks, or stage 1 likely within 24 h: show the
|
||||
// zones unless the user hid them
|
||||
@@ -2408,6 +2509,7 @@
|
||||
try { localStorage.setItem(RAIN_KEY, on ? 'on' : 'off'); } catch (e) { /* private mode */ }
|
||||
renderRain();
|
||||
});
|
||||
$('layer-marks').addEventListener('click', () => setMarks($('layer-marks').getAttribute('aria-pressed') !== 'true'));
|
||||
$('layer-zones').addEventListener('click', () => {
|
||||
const on = $('layer-zones').getAttribute('aria-pressed') !== 'true';
|
||||
state.zonesUserHidden = !on;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"source":"CMU Water Center (ศูนย์วิชาการสนับสนุนการบริหารจัดการน้ำ มหาวิทยาลัยเชียงใหม่), flood marks of October 2024, https://watercenter.cmu.ac.th/chiangmai/cmflood/warning/floodmark","p1_level":5.3,"date":"2024-10-05","fields":["lon","lat","depth_cm_above_road"],"marks":[[98.94555,18.58911,131],[98.94474,18.59004,80],[98.95254,18.59092,35],[98.95091,18.59101,112],[98.95117,18.59133,96],[98.945,18.59134,78],[98.95099,18.5914,114],[98.95584,18.59243,43],[98.95553,18.59256,43],[98.95149,18.59275,156],[98.94926,18.59309,90],[98.94829,18.59436,101],[98.94542,18.59467,65],[98.9414,18.5957,73],[98.94884,18.59594,48],[98.94196,18.59609,63],[98.94342,18.59615,40],[98.94203,18.59616,57],[98.94863,18.59685,68],[98.94599,18.59707,40],[98.93865,18.59717,28],[98.94922,18.5972,76],[98.94842,18.59727,50],[98.94964,18.59769,65],[98.94784,18.59786,50],[98.94939,18.59791,52],[98.94599,18.59823,40],[98.94839,18.59839,37],[98.94795,18.59841,70],[98.94994,18.5985,53],[98.946,18.5986,28],[98.94831,18.59893,37],[98.94721,18.59895,30],[98.94985,18.59897,34],[98.94546,18.59921,38],[98.9493,18.59921,41],[98.94614,18.5994,40],[98.94458,18.59948,36],[98.95012,18.59976,68],[98.94604,18.5998,25],[98.94869,18.59982,32],[98.93946,18.6,26],[98.93307,18.60005,30],[98.94893,18.60033,32],[98.93826,18.60035,26],[98.94578,18.60047,40],[98.96498,18.60066,115],[98.93305,18.60128,58],[98.9535,18.60188,72],[98.96283,18.60198,45],[98.95473,18.60223,81],[98.93342,18.60291,27],[98.93735,18.60399,54],[98.93613,18.60457,32],[98.96806,18.60471,92],[98.93583,18.60479,58],[98.9342,18.60505,32],[98.94897,18.60509,40],[98.94952,18.60523,46],[98.94795,18.60572,63],[98.93452,18.6058,33],[98.93398,18.60581,27],[98.95078,18.60626,60],[98.96559,18.60645,58],[98.96518,18.60728,64],[98.96215,18.61133,54],[98.96336,18.61148,54],[98.96375,18.61238,51],[98.94076,18.61313,15],[98.96247,18.61369,20],[98.98529,18.61605,80],[98.98479,18.61629,73],[98.98601,18.61663,45],[98.96084,18.617,25],[98.98441,18.61702,90],[98.98533,18.61703,120],[98.98649,18.61815,50],[98.95499,18.61817,33],[98.95889,18.61887,35],[99.02179,18.61888,33],[98.98757,18.61924,30],[98.98717,18.61935,63],[98.98659,18.61985,76],[99.0227,18.62004,65],[98.98653,18.62013,79],[98.98854,18.62029,35],[98.9509,18.62036,87],[98.95268,18.62046,62],[99.02302,18.62054,46],[99.02241,18.62074,90],[98.95322,18.62094,96],[98.98795,18.62105,68],[98.95306,18.62117,92],[98.95485,18.62136,34],[99.02224,18.62139,90],[99.02348,18.62149,47],[98.94931,18.6217,83],[98.96125,18.62181,33],[98.98974,18.6219,10],[99.02205,18.62209,110],[98.95239,18.62212,118],[99.02371,18.62217,48],[98.95701,18.62224,20],[98.95168,18.62259,111],[99.02178,18.62272,91],[98.95117,18.62296,115],[99.02411,18.62323,31],[98.95138,18.62404,116],[98.97146,18.62411,26],[98.95172,18.62481,102],[99.02475,18.62491,34],[99.02096,18.62497,87],[98.95873,18.62517,23],[99.02063,18.62561,85],[99.02519,18.62589,18],[99.02027,18.62627,90],[99.02005,18.62656,96],[98.9579,18.62667,39],[99.02561,18.6269,32],[99.01972,18.627,96],[99.02061,18.62716,82],[99.02114,18.62764,52],[99.0192,18.62775,95],[99.03071,18.62795,51],[99.0221,18.6282,44],[98.97386,18.62837,27],[99.01877,18.62841,66],[98.94845,18.6285,73],[99.02274,18.6286,46],[98.97247,18.62869,31],[99.02858,18.62871,72],[99.0264,18.62887,43],[99.02042,18.629,100],[99.02338,18.62904,42],[99.027,18.62927,67],[98.9769,18.62936,43],[99.01764,18.62936,42],[99.02406,18.62941,37],[98.97457,18.62951,47],[99.01819,18.62959,47],[99.02118,18.6296,89],[99.02193,18.63011,89],[99.01882,18.63015,57],[98.95574,18.63018,30],[99.02446,18.63022,26],[99.02145,18.63047,70],[99.02672,18.63061,63],[99.01935,18.63069,65],[99.02963,18.63094,47],[99.02071,18.63096,63],[99.03878,18.63102,13],[98.96853,18.63112,36],[99.02138,18.6313,80],[99.01906,18.63131,36],[99.01855,18.63161,40],[99.02381,18.63165,47],[99.02153,18.63204,56],[99.023,18.63204,46],[99.03029,18.63226,38],[99.02,18.63245,30],[99.02705,18.63245,50],[99.03472,18.63255,18],[99.02185,18.63258,35],[99.0252,18.63336,50],[99.03113,18.63384,30],[99.02535,18.63435,41],[98.97782,18.6344,20],[99.03162,18.63441,47],[99.02745,18.63481,43],[98.98087,18.63494,46],[98.98229,18.63499,20],[99.02544,18.63523,40],[99.03221,18.63524,25],[99.02766,18.6359,47],[99.03261,18.63593,42],[99.05652,18.6362,13],[99.03907,18.63682,52],[98.98704,18.63693,30],[98.96901,18.63711,36],[99.0545,18.63725,9],[98.96356,18.6373,30],[99.03327,18.63757,37],[99.02802,18.63761,36],[98.99439,18.63798,30],[99.02567,18.63811,30],[98.98859,18.63813,36],[98.95419,18.63822,80],[98.98457,18.63837,32],[99.02846,18.63857,24],[98.99412,18.63862,34],[99.0342,18.63895,10],[98.96941,18.63904,30],[99.02548,18.63943,23],[99.03287,18.63954,17],[99.02997,18.6398,25],[99.02907,18.63982,11],[99.03358,18.64016,13],[98.99,18.64047,30],[99.02353,18.64072,41],[99.02159,18.64077,12],[98.99208,18.64086,52],[98.97228,18.64092,25],[99.034Line truncated
|
||||
Reference in new issue
Block a user