CVNSS commited on
Commit
b57b477
·
verified ·
1 Parent(s): 8cf1f44

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +376 -18
index.html CHANGED
@@ -1,19 +1,377 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
6
+ <title>Bản đồ Ranh giới Hành chính Việt Nam – Long Ngo, 2025</title>
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.css"/>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"/>
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.js"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/@turf/turf@6/turf.min.js"></script>
11
+ <style>
12
+ :root{
13
+ --brand-bg: #eef6fa;
14
+ --brand-blue: #1790e0;
15
+ --brand-blue-dark: #0c3a69;
16
+ --brand-blue-light: #b3e5fc;
17
+ --brand-yellow: #ffeb3b;
18
+ --brand-white: #fff;
19
+ --brand-table-border: #90caf9;
20
+ }
21
+ body{font-family:"Segoe UI",Tahoma,Geneva,Verdana,sans-serif;background:var(--brand-bg);color:#174067;min-height:100vh;}
22
+ .header{
23
+ background: linear-gradient(90deg, #e3f2fd 0%, #b3e5fc 100%);
24
+ padding: 1.1rem 2rem 1rem 2rem;
25
+ border-bottom: 4px solid var(--brand-blue);
26
+ box-shadow: 0 2px 16px rgba(23,144,224,.13);
27
+ display: flex; align-items: center; gap: 16px;
28
+ }
29
+ .header h1{font-size:2.1rem;font-weight:bold;color:var(--brand-blue-dark);margin:0;letter-spacing:0.5px;flex:1;}
30
+ .map-controls{
31
+ position:absolute;top:18px;left:18px;z-index:1000;display:flex;flex-direction:column;gap:13px
32
+ }
33
+ .map-controls .box{
34
+ background:var(--brand-white);padding:7px 14px 7px 13px;border-left:5px solid var(--brand-blue);
35
+ border-radius:10px;box-shadow:0 2px 7px rgba(23,144,224,.09)
36
+ }
37
+ .switch{display:inline-flex;align-items:center;gap:9px;font-weight:600;color:var(--brand-blue-dark)}
38
+ .switch .label{font-size:1.04rem}
39
+ .switch input{display:none}
40
+ .switch .slider{
41
+ width:47px;height:23px;border-radius:12px;background:var(--brand-blue-light);position:relative;cursor:pointer;transition:.3s}
42
+ .switch .slider::before{
43
+ content:"";position:absolute;left:2px;top:2px;width:18px;height:18px;border-radius:50%;background:#fff;
44
+ box-shadow:0 1px 5px rgba(23,144,224,.16);transition:.3s}
45
+ .switch input:checked + .slider{background:var(--brand-blue)}
46
+ .switch input:checked + .slider::before{transform:translateX(24px)}
47
+ .basemap-select{margin-left:7px;font-size:1.03rem;border-radius:6px;padding:4px 8px;border:1.2px solid #b3e5fc;background:#fff;color:#1976d2;}
48
+ .download-btn{
49
+ display:inline-block;padding:9px 15px;border-radius:8px;background:var(--brand-blue-dark);
50
+ color:#fff;font-weight:600;text-decoration:none;box-shadow:0 2px 7px rgba(23,144,224,.12)
51
+ }
52
+ .download-btn:hover{background:var(--brand-blue)}
53
+ .leaflet-popup-content-wrapper{
54
+ background:#fff;border:1.6px solid var(--brand-blue-dark);border-radius:10px;
55
+ box-shadow:0 5px 18px rgba(23,144,224,.14)
56
+ }
57
+ .leaflet-popup-close-button{font-size:20px !important;padding:4px 7px;color:#1976d2}
58
+ .leaflet-popup-content{padding:10px 15px !important;font-size:15px;line-height:1.47}
59
+ .popup-title{margin:0 0 6px;font-weight:700;color:var(--brand-blue-dark)}
60
+ .popup-row{display:flex;gap:8px;margin-bottom:5px}
61
+ .popup-row .icon{width:22px;text-align:center}
62
+ .province-label {
63
+ font-size: 13px;
64
+ font-weight: 700;
65
+ color: var(--brand-blue-dark);
66
+ background:rgba(255,255,255,0.88);
67
+ padding:1px 6px 1px 6px;
68
+ border-radius:8px;
69
+ border:1.1px solid #1976d2;
70
+ box-shadow: 0 0 3px #b3e5fc, 0 0 5px #fff;
71
+ pointer-events: none;
72
+ white-space: nowrap;
73
+ letter-spacing: 0.5px;
74
+ }
75
+ .leaflet-tooltip.province-label {
76
+ background: transparent;
77
+ border: none;
78
+ box-shadow: none;
79
+ }
80
+ .leaflet-tooltip.province-label::after { display: none; }
81
+ .compare-bottom{
82
+ position:absolute;left:0;right:0;bottom:0;max-height:42%;overflow:auto;
83
+ background:#fff;border-top:4px solid var(--brand-blue-dark);
84
+ box-shadow:0 -7px 20px rgba(23,144,224,.13);padding:14px 15px 8px 15px;
85
+ border-radius:13px 13px 0 0;z-index:900
86
+ }
87
+ #comparePanel table,#comparePanel th,#comparePanel td{border:1.5px solid var(--brand-table-border)}
88
+ #comparePanel th,#comparePanel td{padding:5px 9px;text-align:center}
89
+ #comparePanel th{background:var(--brand-blue-light);font-size:1.01rem;}
90
+ .compare-remove{cursor:pointer;color:#c00;font-weight:bold}
91
+ .compare-remove:hover{text-decoration:underline}
92
+ .leaflet-control-zoom{display:none !important}
93
+ .footer {
94
+ position: fixed; left:0; right:0; bottom:0; z-index: 99999; background:rgba(240,248,255,0.94);
95
+ color:#0d47a1;font-size:14px;padding:3px 16px;text-align:right;box-shadow:0 -1.5px 10px #b3e5fc;
96
+ letter-spacing:0.5px
97
+ }
98
+ </style>
99
+ </head>
100
+ <body>
101
+ <div class="header">
102
+ <h1>Bản đồ Ranh giới Hành chính Việt Nam</h1>
103
+ <select id="basemapSelect" class="basemap-select" title="Chọn nền bản đồ">
104
+ <option value="satellite">Vệ tinh</option>
105
+ <option value="roadmap">Bản đồ đường</option>
106
+ </select>
107
+ </div>
108
+ <div class="container">
109
+ <div id="map" style="height: 85vh;"></div>
110
+ <div class="map-controls">
111
+ <div class="box">
112
+ <label class="switch">
113
+ <span class="label">Cũ</span>
114
+ <input type="checkbox" id="switchBoundary">
115
+ <span class="slider"></span>
116
+ <span class="label">Mới</span>
117
+ </label>
118
+ </div>
119
+ <button id="toggleCompare" class="download-btn" style="margin-top:7px;">
120
+ Hiện So sánh
121
+ </button>
122
+ </div>
123
+ <div id="comparePanel" class="compare-bottom" style="display:none">
124
+ <h3><i class="fas fa-balance-scale"></i> So sánh (0/5)</h3>
125
+ <div id="compareTable" style="font-size:13px"></div>
126
+ </div>
127
+ </div>
128
+ <div class="footer">Nguồn: Long Ngo, 2025.</div>
129
+ <script>
130
+ const CONFIG = {
131
+ old:"https://raw.githubusercontent.com/lqtue/LacaProvinceMap/main/old.geojson",
132
+ new:"https://raw.githubusercontent.com/lqtue/LacaProvinceMap/main/new.geojson"
133
+ };
134
+ let map, oldLayer, newLayer;
135
+ let baseLayerSatellite, baseLayerRoad;
136
+ const dataStore = {old:null,new:null};
137
+ let compareList=[];
138
+
139
+ const iconMap = {
140
+ "Tỉnh thành mới": "📍", "Tỉnh thành cũ": "📍", "TT hành chính": "📍",
141
+ "GRDP 2024 (tỷ VND)": "💰", "Thu ngân sách 2024 (tỷ VND)": "💰",
142
+ "Diện tích (km2)": "🗺️", "Dân số": "👥", "ĐVHC cấp xã": "🏛️"
143
+ };
144
+ const labelOverrides = {
145
+ "Khánh Hoà": [12.248126980225129, 109.183807743233],
146
+ "TP HCM": [10.801867540653552, 106.68102175169227],
147
+ "TP. Hồ Chí Minh": [10.801867540653552, 106.68102175169227]
148
+ };
149
+ const numFmt = v=>{
150
+ const s = String(v).replace(/,/g, '.').replace(/ /g, '');
151
+ const n = parseFloat(s);
152
+ return isNaN(n) ? v : n.toLocaleString('vi-VN');
153
+ };
154
+ const ATTRS = ["Diện tích (km2)","Dân số","GRDP 2024 (tỷ VND)","Thu ngân sách 2024 (tỷ VND)","ĐVHC cấp xã"];
155
+ const numAttrs = ["Diện tích (km2)","Dân số","GRDP 2024 (tỷ VND)","Thu ngân sách 2024 (tỷ VND)"];
156
+ const palette = [
157
+ '#1565c0','#1976d2','#0277bd','#00838f','#0288d1','#00b8d4',
158
+ '#00897b','#00695c','#039be5','#00acc1','#0097a7','#01579b',
159
+ '#1e88e5','#29b6f6','#26c6da','#26a69a','#388e3c','#2e7d32',
160
+ '#4caf50','#0288d1','#2b32b2','#134e5e','#56ab2f','#0f2027',
161
+ '#43cea2','#185a9d','#283e51','#4776e6','#36d1c4','#159957'
162
+ ];
163
+ function getProvinceColour(name){
164
+ let hash = 0;
165
+ for(let i=0;i<name.length;i++) hash = name.charCodeAt(i)+(hash<<5)-hash;
166
+ const idx = Math.abs(hash) % palette.length;
167
+ return palette[idx];
168
+ }
169
+ function init(){
170
+ map = L.map('map',{zoomControl:false, attributionControl:false}).setView([16,108],6);
171
+ baseLayerSatellite = L.tileLayer('https://{s}.google.com/vt/lyrs=s&x={x}&y={y}&z={z}',
172
+ {subdomains:['mt0','mt1','mt2','mt3'], attribution:'© Google Satellite'});
173
+ baseLayerRoad = L.tileLayer('https://{s}.google.com/vt/lyrs=m&x={x}&y={y}&z={z}',
174
+ {subdomains:['mt0','mt1','mt2','mt3'], attribution:'© Google Maps'});
175
+ baseLayerSatellite.addTo(map);
176
+
177
+ L.control.attribution({prefix: ''}).addTo(map);
178
+ map.attributionControl.setPrefix('© Google | Leaflet');
179
+ oldLayer=L.layerGroup().addTo(map);
180
+ newLayer=L.layerGroup().addTo(map);
181
+
182
+ fetch(CONFIG.old).then(r=>r.json()).then(gOld=>{
183
+ dataStore.old=gOld; drawLayer(gOld,'old');
184
+ });
185
+ fetch(CONFIG.new).then(r=>r.json()).then(gNew=>{
186
+ dataStore.new=gNew; drawLayer(gNew,'new');
187
+ if (gNew && gNew.features && gNew.features.length > 0) {
188
+ map.fitBounds(L.geoJSON(gNew).getBounds());
189
+ }
190
+ });
191
+
192
+ const sw=document.getElementById('switchBoundary');
193
+ sw.checked=true; map.removeLayer(oldLayer);
194
+ sw.onchange=()=> {
195
+ if (sw.checked) {
196
+ map.removeLayer(oldLayer); map.addLayer(newLayer);
197
+ } else {
198
+ map.removeLayer(newLayer); map.addLayer(oldLayer);
199
+ }
200
+ updateLabels();
201
+ };
202
+ document.getElementById('basemapSelect').onchange = function() {
203
+ if (this.value==="satellite") {
204
+ map.removeLayer(baseLayerRoad); map.addLayer(baseLayerSatellite);
205
+ } else {
206
+ map.removeLayer(baseLayerSatellite); map.addLayer(baseLayerRoad);
207
+ }
208
+ };
209
+ }
210
+ function drawLayer(gjson, type) {
211
+ const layer = type === 'old' ? oldLayer : newLayer;
212
+ layer.clearLayers();
213
+ L.geoJSON(gjson, {
214
+ style: feature => {
215
+ const provName = feature.properties["Tỉnh thành mới"] || feature.properties["Tỉnh thành cũ"];
216
+ return {
217
+ color: '#ffeb3b',
218
+ weight: 2.7,
219
+ opacity: 1,
220
+ fillColor: getProvinceColour(provName),
221
+ fillOpacity: 0.73
222
+ };
223
+ },
224
+ onEachFeature: (f, ly) => {
225
+ const p = f.properties;
226
+ const title = (p["Tỉnh thành mới"] || p["Tỉnh thành cũ"]) || 'Thông tin';
227
+ let html = `<h4 class="popup-title">${title}</h4>`;
228
+ [
229
+ "Tỉnh thành mới", "Tỉnh thành cũ", "TT hành chính", ...ATTRS
230
+ ].forEach(k => {
231
+ if (p[k]) {
232
+ const val = numAttrs.includes(k) ? numFmt(p[k]) : p[k];
233
+ html += `<div class="popup-row"><span class="icon">${iconMap[k] || ''}</span>
234
+ <strong>${k}:</strong> <span>${val}</span></div>`;
235
+ }
236
+ });
237
+ const key = (type === 'old' ? 'old:' : 'new:') + title;
238
+ html += `<button class="addCompare" data-key="${key}" data-type="${type}"
239
+ style="margin-top:8px;padding:6px 10px;border:none;border-radius:6px;
240
+ background:var(--brand-blue);color:#fff;cursor:pointer">
241
+ + Thêm so sánh</button>`;
242
+ ly.bindPopup(html);
243
+ const labelText = type === 'old'
244
+ ? (p["Tỉnh thành cũ"] || p["Tỉnh thành mới"])
245
+ : (p["Tỉnh thành mới"] || p["Tỉnh thành cũ"]);
246
+ let labelPosition = null;
247
+ const featureGeometry = f.geometry;
248
+ if (labelText && labelOverrides[labelText]) {
249
+ labelPosition = labelOverrides[labelText];
250
+ }
251
+ else if (labelText && featureGeometry) {
252
+ let labelPolygonFeature = null;
253
+ if (featureGeometry.type === 'Polygon') {
254
+ labelPolygonFeature = turf.polygon(featureGeometry.coordinates);
255
+ } else if (featureGeometry.type === 'MultiPolygon') {
256
+ let largestArea = 0;
257
+ featureGeometry.coordinates.forEach(polygonCoords => {
258
+ const poly = turf.polygon(polygonCoords);
259
+ const area = turf.area(poly);
260
+ if (area > largestArea) {
261
+ largestArea = area;
262
+ labelPolygonFeature = poly;
263
+ }
264
+ });
265
+ }
266
+ if (labelPolygonFeature) {
267
+ try {
268
+ const pointOnSurface = turf.pointOnFeature(labelPolygonFeature);
269
+ if (pointOnSurface && pointOnSurface.geometry && pointOnSurface.geometry.coordinates) {
270
+ labelPosition = [pointOnSurface.geometry.coordinates[1], pointOnSurface.geometry.coordinates[0]];
271
+ }
272
+ } catch (e) {
273
+ const bounds = ly.getBounds();
274
+ if (bounds.isValid()) {
275
+ labelPosition = bounds.getCenter();
276
+ }
277
+ }
278
+ }
279
+ }
280
+ const tooltipOptions = {
281
+ permanent: true,
282
+ direction: 'center',
283
+ className: 'province-label',
284
+ pane: 'tooltipPane'
285
+ };
286
+ if (labelPosition) {
287
+ const customTooltip = L.tooltip(tooltipOptions)
288
+ .setLatLng(labelPosition)
289
+ .setContent(labelText);
290
+ ly.bindTooltip(customTooltip);
291
+ } else if (labelText) {
292
+ ly.bindTooltip(labelText, tooltipOptions);
293
+ }
294
+ ly.on('mouseover', e => e.target.setStyle({ fillOpacity: 0.88, weight: 3.3 }));
295
+ ly.on('mouseout', e => e.target.setStyle({ fillOpacity: 0.73, weight: 2.7 }));
296
+ }
297
+ }).addTo(layer);
298
+ }
299
+ function updateCompare(){
300
+ const panel=document.getElementById('comparePanel');
301
+ const tgt=document.getElementById('compareTable');
302
+ if(!compareList.length){panel.style.display='none';return;}
303
+ panel.style.display='block';
304
+ panel.querySelector('h3').innerHTML=
305
+ `<i class="fas fa-balance-scale"></i> So sánh (${compareList.length}/5)`;
306
+ let html='<table style="width:100%;border-collapse:collapse"><tr><th>Thuộc tính</th>';
307
+ compareList.forEach(c=>{
308
+ const nm=c.props["Tỉnh thành mới"]||c.props["Tỉnh thành cũ"];
309
+ html+=`<th>${nm}<br><span style="font-size:11px;color:#666">(${c.layer==='old'?'Cũ':'Mới'})</span>
310
+ <span class="compare-remove" data-k="${c.key}">×</span></th>`;
311
+ });
312
+ html+='</tr>';
313
+ ATTRS.forEach(a=>{
314
+ html+=`<tr><td><strong>${a}</strong></td>`;
315
+ compareList.forEach(c=>{
316
+ let v=c.props[a]??'--';
317
+ if(numAttrs.includes(a)) v=numFmt(v);
318
+ html+=`<td>${v}</td>`;
319
+ });
320
+ html+='</tr>';
321
+ });
322
+ html+='</table>';
323
+ tgt.innerHTML=html;
324
+ tgt.querySelectorAll('.compare-remove').forEach(x=>{
325
+ x.onclick=()=>{compareList=compareList.filter(c=>c.key!==x.dataset.k);updateCompare();};
326
+ });
327
+ }
328
+ function attachCompare(){
329
+ map.on('popupopen',e=>{
330
+ const btn=e.popup._contentNode.querySelector('.addCompare');
331
+ if(!btn) return;
332
+ btn.onclick=()=>{
333
+ const props=e.popup._source.feature.properties;
334
+ const key=btn.dataset.key, layerType=btn.dataset.type;
335
+ if(compareList.find(c=>c.key===key)) return;
336
+ if(compareList.length>=5){alert('Tối đa 5 mục so sánh.');return;}
337
+ compareList.push({key,props,layer:layerType});
338
+ updateCompare();
339
+ };
340
+ });
341
+ }
342
+ function updateLabels() {
343
+ const zoom = map.getZoom();
344
+ map.eachLayer(layer => {
345
+ if (layer.getTooltip && layer.getTooltip()) {
346
+ const tooltip = layer.getTooltip();
347
+ if (tooltip.options.className && tooltip.options.className.includes('province-label')) {
348
+ const el = tooltip.getElement();
349
+ if (el) {
350
+ if (zoom < 7) {el.style.display = 'none';}
351
+ else {el.style.display = 'block'; el.style.fontSize = Math.min(Math.max(zoom * 1.5, 12), 22) + 'px';}
352
+ }
353
+ }
354
+ }
355
+ });
356
+ }
357
+ document.addEventListener('DOMContentLoaded',()=>{
358
+ init(); attachCompare();
359
+ map && map.whenReady(() => { updateLabels(); });
360
+ map && map.on('zoomend', updateLabels);
361
+ map && map.on('layeradd layerremove', updateLabels);
362
+ const toggleBtn = document.getElementById('toggleCompare');
363
+ const comparePanel = document.getElementById('comparePanel');
364
+ toggleBtn.addEventListener('click', () => {
365
+ const isHidden = comparePanel.style.display === 'none' || comparePanel.style.display === '';
366
+ if (isHidden) {
367
+ comparePanel.style.display = 'block';
368
+ toggleBtn.innerHTML = '<i class="fas fa-eye-slash"></i> Ẩn So sánh';
369
+ } else {
370
+ comparePanel.style.display = 'none';
371
+ toggleBtn.innerHTML = '<i class="fas fa-eye"></i> Hiện So sánh';
372
+ }
373
+ });
374
+ });
375
+ </script>
376
+ </body>
377
  </html>