Initial commit of antigravity2 project
This commit is contained in:
@@ -0,0 +1,621 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pontianak WebGIS - Cloropleth & Layers</title>
|
||||
|
||||
<!-- Leaflet CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
|
||||
|
||||
<!-- Leaflet JS -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
|
||||
<!-- Proj4js for UTM Zone 49S to WGS84 coordinates projection -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.9.0/proj4.js"></script>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
#map {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Premium Floating Header overlay */
|
||||
.map-header {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1000;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 10px 24px;
|
||||
border-radius: 30px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
text-align: center;
|
||||
pointer-events: auto;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.map-header h1 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.map-header p {
|
||||
margin: 2px 0 0 0;
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Glassmorphic Layer Control custom overrides */
|
||||
.leaflet-control-layers {
|
||||
background: rgba(255, 255, 255, 0.85) !important;
|
||||
backdrop-filter: blur(10px) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.25) !important;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15) !important;
|
||||
font-family: 'Segoe UI', sans-serif !important;
|
||||
padding: 10px 14px !important;
|
||||
}
|
||||
|
||||
.leaflet-control-layers-list label {
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
color: #444;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.leaflet-control-layers-list input {
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Custom zoom controls alignment */
|
||||
.leaflet-bar {
|
||||
border: none !important;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15) !important;
|
||||
border-radius: 10px !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.leaflet-bar a {
|
||||
background-color: rgba(255, 255, 255, 0.85) !important;
|
||||
backdrop-filter: blur(10px) !important;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1) !important;
|
||||
color: #333 !important;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.leaflet-bar a:hover {
|
||||
background-color: #ffffff !important;
|
||||
color: #007bff !important;
|
||||
}
|
||||
|
||||
/* Loading indicator spinner */
|
||||
#loader {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: #ffffff;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 5px solid rgba(0, 123, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
border-top-color: #007bff;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Spinner Loader -->
|
||||
<div id="loader">
|
||||
<div class="spinner"></div>
|
||||
<div class="loading-text">Loading WebGIS Environment...</div>
|
||||
</div>
|
||||
|
||||
<!-- Floating Top Header Overlay -->
|
||||
<div class="map-header">
|
||||
<h1>Pontianak City WebGIS</h1>
|
||||
<p>Interactive Cloropleth Administration Map</p>
|
||||
</div>
|
||||
|
||||
<!-- CORS Server Warning Overlay fall back -->
|
||||
<div id="cors-warning"
|
||||
style="display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.75); backdrop-filter: blur(8px); z-index: 99999; justify-content: center; align-items: center; color: white;">
|
||||
<div
|
||||
style="background: rgba(30, 30, 30, 0.95); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 16px; padding: 30px; max-width: 500px; text-align: center; box-shadow: 0 20px 50px rgba(0,0,0,0.6); font-family: 'Segoe UI', sans-serif;">
|
||||
<div style="font-size: 54px; margin-bottom: 15px;">🌐</div>
|
||||
<h3 style="margin-top: 0; font-size: 22px; font-weight: 600; color: #ff7800;">Local Web Server Required</h3>
|
||||
<p style="font-size: 14px; line-height: 1.6; color: #ddd; margin-bottom: 20px;">
|
||||
Browser security blocks loading local GeoJSON data files directly from a <code>file://</code> URL.
|
||||
</p>
|
||||
<div
|
||||
style="font-size: 13.5px; line-height: 1.6; color: #ccc; margin-bottom: 25px; text-align: left; background: rgba(0,0,0,0.4); padding: 15px; border-radius: 8px; border-left: 4px solid #ff7800;">
|
||||
<strong>Quick Fixes to run locally:</strong><br>
|
||||
1. If in VS Code, click the <strong>"Go Live"</strong> button in the bottom right corner (Live
|
||||
Server).<br>
|
||||
2. Run <code>python -m http.server</code> in this folder and visit
|
||||
<code>http://localhost:8000</code>.<br>
|
||||
3. Run <code>npx serve</code> in this directory in your terminal.
|
||||
</div>
|
||||
<button onclick="document.getElementById('cors-warning').style.display='none'"
|
||||
style="background: #ff7800; border: none; color: white; padding: 12px 28px; font-size: 14px; font-weight: 600; border-radius: 8px; cursor: pointer; transition: background 0.3s; box-shadow: 0 4px 12px rgba(255, 120, 0, 0.3);">Dismiss</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leaflet Container -->
|
||||
<div id="map"></div>
|
||||
|
||||
<script>
|
||||
// ------------------------------------------------------------------
|
||||
// INITIALIZE MAP & BASE LAYERS
|
||||
// ------------------------------------------------------------------
|
||||
var map = L.map('map', {
|
||||
zoomControl: false,
|
||||
attributionControl: true
|
||||
}).setView([-0.0258, 109.3323], 13); // Centered around Pontianak
|
||||
|
||||
// Zoom control in bottom right for cleaner aesthetics
|
||||
L.control.zoom({ position: 'bottomleft' }).addTo(map);
|
||||
|
||||
// OSM Map Layer
|
||||
var osmLayer = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// PROJ4 COORDINATE CONVERSION (UTM Zone 49S to WGS84)
|
||||
// ------------------------------------------------------------------
|
||||
// Define UTM Zone 49S (which Pontianak falls into) and standard Lat/Lng projections
|
||||
const utm49S = "+proj=utm +zone=49 +south +datum=WGS84 +units=m +no_defs";
|
||||
const wgs84 = "+proj=longlat +datum=WGS84 +no_defs";
|
||||
|
||||
// Dynamic conversion logic for various GeoJSON geometry types
|
||||
function convertCoords(coords, type) {
|
||||
const isLatLng = (c) => Math.abs(c[0]) <= 180 && Math.abs(c[1]) <= 90;
|
||||
|
||||
if (type === "Point") {
|
||||
if (isLatLng(coords)) return coords;
|
||||
const [lng, lat] = proj4(utm49S, wgs84, coords);
|
||||
return [lng, lat];
|
||||
} else if (type === "LineString" || type === "MultiPoint") {
|
||||
if (coords.length > 0 && isLatLng(coords[0])) return coords;
|
||||
return coords.map(c => {
|
||||
const [lng, lat] = proj4(utm49S, wgs84, c);
|
||||
return [lng, lat];
|
||||
});
|
||||
} else if (type === "Polygon" || type === "MultiLineString") {
|
||||
if (coords.length > 0 && coords[0].length > 0 && isLatLng(coords[0][0])) return coords;
|
||||
return coords.map(ring =>
|
||||
ring.map(c => {
|
||||
const [lng, lat] = proj4(utm49S, wgs84, c);
|
||||
return [lng, lat];
|
||||
})
|
||||
);
|
||||
} else if (type === "MultiPolygon") {
|
||||
if (coords.length > 0 && coords[0].length > 0 && coords[0][0].length > 0 && isLatLng(coords[0][0][0])) return coords;
|
||||
return coords.map(polygon =>
|
||||
polygon.map(ring =>
|
||||
ring.map(c => {
|
||||
const [lng, lat] = proj4(utm49S, wgs84, c);
|
||||
return [lng, lat];
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
return coords;
|
||||
}
|
||||
|
||||
// Deep copies the GeoJSON and converts coordinates recursively
|
||||
function convertGeoJSON(geojson) {
|
||||
if (!geojson) return geojson;
|
||||
const cloned = JSON.parse(JSON.stringify(geojson));
|
||||
|
||||
const convertFeature = (feature) => {
|
||||
if (feature.geometry && feature.geometry.coordinates) {
|
||||
feature.geometry.coordinates = convertCoords(
|
||||
feature.geometry.coordinates,
|
||||
feature.geometry.type
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (cloned.type === "FeatureCollection" && cloned.features) {
|
||||
cloned.features.forEach(convertFeature);
|
||||
} else if (cloned.type === "Feature") {
|
||||
cloned.features = [cloned];
|
||||
cloned.type = "FeatureCollection";
|
||||
cloned.features.forEach(convertFeature);
|
||||
}
|
||||
return cloned;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// DUMMY POPULATION DATA — Kecamatan Pontianak (2024 Estimate)
|
||||
// ------------------------------------------------------------------
|
||||
const populationData = {
|
||||
'Pontianak Timur': { population: 108_450, area_km2: 8.72, density: 12437 },
|
||||
'Pontianak Selatan': { population: 87_320, area_km2: 14.54, density: 6005 },
|
||||
'Pontianak Barat': { population: 142_610, area_km2: 16.81, density: 8484 },
|
||||
'Pontianak Kota': { population: 65_180, area_km2: 5.13, density: 12706 },
|
||||
'Pontianak Utara': { population: 98_760, area_km2: 40.94, density: 2412 },
|
||||
'Pontianak Tenggara': { population: 73_540, area_km2: 11.26, density: 6531 }
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// CHOROPLETH COLOR SCALE — Sequential blue-green by population
|
||||
// ------------------------------------------------------------------
|
||||
// Breaks based on population ranges across the 6 kecamatan
|
||||
function getColor(ket) {
|
||||
const data = populationData[ket];
|
||||
if (!data) return '#8E9AAF';
|
||||
const pop = data.population;
|
||||
return pop > 130_000 ? '#084594' :
|
||||
pop > 110_000 ? '#2171b5' :
|
||||
pop > 95_000 ? '#4292c6' :
|
||||
pop > 80_000 ? '#6baed6' :
|
||||
pop > 65_000 ? '#9ecae1' :
|
||||
'#c6dbef';
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// LAYER GEOMETRY STYLING & INTERACTION Handlers
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
// 1. Administrative Kecamatan styling & interaction
|
||||
function styleAdminKecamatan(feature) {
|
||||
return {
|
||||
fillColor: getColor(feature.properties.Ket),
|
||||
weight: 1.5,
|
||||
opacity: 1,
|
||||
color: 'white',
|
||||
dashArray: '',
|
||||
fillOpacity: 0.5
|
||||
};
|
||||
}
|
||||
|
||||
function highlightFeature(e) {
|
||||
var layer = e.target;
|
||||
layer.setStyle({
|
||||
weight: 3,
|
||||
color: '#666',
|
||||
dashArray: '',
|
||||
fillOpacity: 0.7
|
||||
});
|
||||
layer.bringToFront();
|
||||
info.update(layer.feature.properties);
|
||||
}
|
||||
|
||||
function resetHighlight(e) {
|
||||
adminKecamatanLayer.resetStyle(e.target);
|
||||
info.update();
|
||||
}
|
||||
|
||||
function onEachFeatureAdmin(feature, layer) {
|
||||
layer.on({
|
||||
mouseover: highlightFeature,
|
||||
mouseout: resetHighlight,
|
||||
click: function (e) {
|
||||
map.fitBounds(e.target.getBounds());
|
||||
}
|
||||
});
|
||||
|
||||
if (feature.properties && feature.properties.Ket) {
|
||||
const ket = feature.properties.Ket;
|
||||
const pop = populationData[ket];
|
||||
const popStr = pop ? pop.population.toLocaleString('id-ID') : 'N/A';
|
||||
const areaStr = pop ? pop.area_km2.toFixed(2) : ((feature.properties.Plan_Area / 1000000).toFixed(2));
|
||||
const densStr = pop ? pop.density.toLocaleString('id-ID') : 'N/A';
|
||||
const color = getColor(ket);
|
||||
|
||||
layer.bindPopup(`
|
||||
<div style="font-family: 'Segoe UI', sans-serif; padding: 3px; min-width: 200px;">
|
||||
<div style="display:flex; align-items:center; gap:8px; margin-bottom:8px; border-bottom: 1.5px solid #eee; padding-bottom:6px;">
|
||||
<span style="display:inline-block; width:12px; height:12px; border-radius:3px; background:${color}; flex-shrink:0;"></span>
|
||||
<h4 style="margin:0; color:#111; font-weight:700; font-size:13.5px;">${ket}</h4>
|
||||
</div>
|
||||
<table style="width:100%; border-collapse:collapse; font-size:12px; color:#444;">
|
||||
<tr>
|
||||
<td style="padding:3px 0;">👥 <strong>Jumlah Penduduk</strong></td>
|
||||
<td style="padding:3px 0; text-align:right; color:#2171b5; font-weight:700;">${popStr} jiwa</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:3px 0;">📐 <strong>Luas Wilayah</strong></td>
|
||||
<td style="padding:3px 0; text-align:right; color:#555; font-weight:600;">${areaStr} km²</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:3px 0;">🏙️ <strong>Kepadatan</strong></td>
|
||||
<td style="padding:3px 0; text-align:right; color:#555; font-weight:600;">${densStr} jiwa/km²</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div style="margin-top:8px; font-size:10.5px; color:#aaa; text-align:right;">*Data Estimasi 2024</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
// Declaring Leaflet layers as placeholder layer groups
|
||||
var adminKecamatanLayer = L.geoJSON(null);
|
||||
var batasKecamatanLayer = L.geoJSON(null);
|
||||
var batasWilayahLayer = L.geoJSON(null);
|
||||
var poligonKotaLayer = L.geoJSON(null);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// INTERACTIVE HUD CONTROLS (District Info Panel & Legend)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
// Custom Hover HUD Info Panel
|
||||
var info = L.control({ position: 'topright' });
|
||||
info.onAdd = function (map) {
|
||||
this._div = L.DomUtil.create('div', 'info');
|
||||
this._div.style.background = 'rgba(255, 255, 255, 0.85)';
|
||||
this._div.style.backdropFilter = 'blur(10px)';
|
||||
this._div.style.padding = '12px 18px';
|
||||
this._div.style.borderRadius = '14px';
|
||||
this._div.style.boxShadow = '0 10px 30px rgba(0, 0, 0, 0.15)';
|
||||
this._div.style.border = '1px solid rgba(255, 255, 255, 0.25)';
|
||||
this._div.style.fontSize = '13px';
|
||||
this._div.style.color = '#333';
|
||||
this._div.style.minWidth = '200px';
|
||||
this._div.style.fontFamily = "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif";
|
||||
this.update();
|
||||
return this._div;
|
||||
};
|
||||
info.update = function (props) {
|
||||
if (!props) {
|
||||
this._div.innerHTML = '<h4 style="margin: 0 0 8px 0; font-size: 13.5px; font-weight: 700; color: #111; border-bottom: 1px solid rgba(0,0,0,0.06); padding-bottom: 4px;">Informasi Kecamatan</h4>' +
|
||||
'<span style="color:#777; font-style:italic; font-size:12px;">Arahkan kursor ke kecamatan</span>';
|
||||
return;
|
||||
}
|
||||
const ket = props.Ket;
|
||||
const pop = populationData[ket];
|
||||
const popStr = pop ? pop.population.toLocaleString('id-ID') : '—';
|
||||
const areaStr = pop ? pop.area_km2.toFixed(2) : (props.Plan_Area ? (props.Plan_Area / 1000000).toFixed(2) : '—');
|
||||
const densStr = pop ? pop.density.toLocaleString('id-ID') : '—';
|
||||
const color = getColor(ket);
|
||||
this._div.innerHTML =
|
||||
'<h4 style="margin: 0 0 8px 0; font-size: 13.5px; font-weight: 700; color: #111; border-bottom: 1px solid rgba(0,0,0,0.06); padding-bottom: 4px;">Informasi Kecamatan</h4>' +
|
||||
`<div style="display:flex; align-items:center; gap:7px; margin-bottom:6px;">
|
||||
<span style="display:inline-block; width:11px; height:11px; border-radius:3px; background:${color}; flex-shrink:0;"></span>
|
||||
<b style="font-size: 14px; color: #1a1a2e; font-weight: 700;">${ket}</b>
|
||||
</div>
|
||||
<table style="width:100%; font-size:12px; color:#444; border-collapse:collapse;">
|
||||
<tr>
|
||||
<td style="padding:2px 0;">👥 Penduduk</td>
|
||||
<td style="padding:2px 0; text-align:right; color:#2171b5; font-weight:700;">${popStr} jiwa</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:2px 0;">📐 Luas</td>
|
||||
<td style="padding:2px 0; text-align:right; font-weight:600;">${areaStr} km²</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:2px 0;">🏙️ Kepadatan</td>
|
||||
<td style="padding:2px 0; text-align:right; font-weight:600;">${densStr} jiwa/km²</td>
|
||||
</tr>
|
||||
</table>`;
|
||||
};
|
||||
info.addTo(map);
|
||||
|
||||
// Custom Choropleth Population Legend Control
|
||||
var legend = L.control({ position: 'bottomright' });
|
||||
legend.onAdd = function (map) {
|
||||
var div = L.DomUtil.create('div', 'info legend');
|
||||
div.style.background = 'rgba(255, 255, 255, 0.9)';
|
||||
div.style.backdropFilter = 'blur(10px)';
|
||||
div.style.padding = '14px 18px';
|
||||
div.style.borderRadius = '14px';
|
||||
div.style.boxShadow = '0 10px 30px rgba(0, 0, 0, 0.15)';
|
||||
div.style.border = '1px solid rgba(255, 255, 255, 0.25)';
|
||||
div.style.fontSize = '12px';
|
||||
div.style.color = '#333';
|
||||
div.style.fontFamily = "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif";
|
||||
div.style.minWidth = '195px';
|
||||
|
||||
// Population break labels (matching getColor thresholds)
|
||||
const grades = [0, 65_001, 80_001, 95_001, 110_001, 130_001];
|
||||
const colors = ['#c6dbef', '#9ecae1', '#6baed6', '#4292c6', '#2171b5', '#084594'];
|
||||
const labels = [
|
||||
'< 65.000',
|
||||
'65.001 – 80.000',
|
||||
'80.001 – 95.000',
|
||||
'95.001 – 110.000',
|
||||
'110.001 – 130.000',
|
||||
'> 130.000'
|
||||
];
|
||||
|
||||
var html = '<h4 style="margin: 0 0 10px 0; font-size: 13px; font-weight: 700; color: #111; border-bottom: 2px solid #eaeaea; padding-bottom: 6px;">🗺️ Legenda Penduduk</h4>';
|
||||
html += '<div style="font-size: 11px; color: #888; margin-bottom: 8px;">Jumlah Penduduk (jiwa)</div>';
|
||||
|
||||
colors.forEach((color, i) => {
|
||||
html += `
|
||||
<div style="display: flex; align-items: center; margin-bottom: 5px;">
|
||||
<span style="display:inline-block; width:16px; height:16px; margin-right:10px; border-radius:4px; background:${color}; border:1px solid rgba(0,0,0,0.1); box-shadow: 0 1px 3px rgba(0,0,0,0.1); flex-shrink:0;"></span>
|
||||
<span style="font-weight:500; color:#444; font-size:11.5px;">${labels[i]}</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
// Per-kecamatan summary table
|
||||
html += '<div style="margin-top:12px; border-top:1px solid #eee; padding-top:10px;">';
|
||||
html += '<div style="font-size:11px; font-weight:700; color:#555; margin-bottom:6px;">Data Per Kecamatan</div>';
|
||||
const sortedKec = Object.entries(populationData).sort((a, b) => b[1].population - a[1].population);
|
||||
sortedKec.forEach(([name, data]) => {
|
||||
const shortName = name.replace('Pontianak ', '');
|
||||
html += `
|
||||
<div style="display:flex; align-items:center; gap:7px; margin-bottom:4px;">
|
||||
<span style="display:inline-block; width:10px; height:10px; border-radius:2px; background:${getColor(name)}; flex-shrink:0;"></span>
|
||||
<span style="font-size:11px; color:#555; flex:1;">${shortName}</span>
|
||||
<span style="font-size:11px; font-weight:700; color:#2171b5;">${data.population.toLocaleString('id-ID')}</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
div.innerHTML = html;
|
||||
return div;
|
||||
};
|
||||
legend.addTo(map);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// FETCH DATA & INTEGRATE LAYERS
|
||||
// ------------------------------------------------------------------
|
||||
async function loadGeoJSONLayers() {
|
||||
try {
|
||||
// Fetch all four local GeoJSON files simultaneously
|
||||
const [adminRes, batasKecRes, batasWilRes, poligonRes] = await Promise.all([
|
||||
fetch('Admin_Kecamatan.json'),
|
||||
fetch('Batas_Kecamatan.json'),
|
||||
fetch('Batas_Wilayah.json'),
|
||||
fetch('Poligon_Kota.json')
|
||||
]);
|
||||
|
||||
// Ensure HTTP calls were successful
|
||||
if (!adminRes.ok || !batasKecRes.ok || !batasWilRes.ok || !poligonRes.ok) {
|
||||
throw new Error("HTTP resource fetching failed.");
|
||||
}
|
||||
|
||||
// Parse into JSON format
|
||||
const rawAdmin = await adminRes.json();
|
||||
const rawBatasKec = await batasKecRes.json();
|
||||
const rawBatasWil = await batasWilRes.json();
|
||||
const rawPoligon = await poligonRes.json();
|
||||
|
||||
// Convert UTM coordinates to WGS84 recursively
|
||||
const adminData = convertGeoJSON(rawAdmin);
|
||||
const batasKecData = convertGeoJSON(rawBatasKec);
|
||||
const batasWilData = convertGeoJSON(rawBatasWil);
|
||||
const poligonData = convertGeoJSON(rawPoligon);
|
||||
|
||||
// --- 1. ADMINISTRATIVE KECAMATAN LAYER ---
|
||||
adminKecamatanLayer = L.geoJSON(adminData, {
|
||||
style: styleAdminKecamatan,
|
||||
onEachFeature: onEachFeatureAdmin
|
||||
}).addTo(map);
|
||||
|
||||
// --- 2. DISTRICT BOUNDARIES LAYER ---
|
||||
batasKecamatanLayer = L.geoJSON(batasKecData, {
|
||||
style: {
|
||||
color: '#495057', // Slate dark boundary color
|
||||
weight: 2,
|
||||
dashArray: '5, 5',
|
||||
opacity: 0.85
|
||||
}
|
||||
}).addTo(map);
|
||||
|
||||
// --- 3. TERRITORY BOUNDARIES LAYER ---
|
||||
batasWilayahLayer = L.geoJSON(batasWilRes, {
|
||||
style: {
|
||||
color: '#e67e22', // Vivid orange boundary
|
||||
weight: 3,
|
||||
opacity: 0.9
|
||||
}
|
||||
}).addTo(map);
|
||||
|
||||
// --- 4. CITY POLIGON MUNICIPAL LAYER ---
|
||||
poligonKotaLayer = L.geoJSON(poligonData, {
|
||||
style: {
|
||||
color: '#2d3748',
|
||||
weight: 1.5,
|
||||
fillColor: '#cbd5e0',
|
||||
fillOpacity: 0.15
|
||||
}
|
||||
}).addTo(map);
|
||||
|
||||
// Combine bounds from all active layers to focus correctly
|
||||
const combinedBounds = L.latLngBounds();
|
||||
[adminKecamatanLayer, batasKecamatanLayer, batasWilayahLayer, poligonKotaLayer].forEach(layer => {
|
||||
if (layer.getBounds().isValid()) {
|
||||
combinedBounds.extend(layer.getBounds());
|
||||
}
|
||||
});
|
||||
|
||||
if (combinedBounds.isValid()) {
|
||||
map.fitBounds(combinedBounds, { padding: [30, 30] });
|
||||
}
|
||||
|
||||
// Add glassmorphic base layer toggle control panel
|
||||
var baseMaps = {
|
||||
"OpenStreetMap": osmLayer
|
||||
};
|
||||
|
||||
var overlayMaps = {
|
||||
"Administrative districts": adminKecamatanLayer,
|
||||
"District boundaries": batasKecamatanLayer,
|
||||
"Territory boundary lines": batasWilayahLayer,
|
||||
"City polygon limits": poligonKotaLayer
|
||||
};
|
||||
|
||||
L.control.layers(baseMaps, overlayMaps, { collapsed: false }).addTo(map);
|
||||
|
||||
// Transition loader off screen
|
||||
const loader = document.getElementById('loader');
|
||||
loader.style.opacity = '0';
|
||||
setTimeout(() => loader.style.display = 'none', 500);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error loading WebGIS layers:", error);
|
||||
|
||||
// Remove loader spinner and show elegant CORS server overlay details
|
||||
document.getElementById('loader').style.display = 'none';
|
||||
document.getElementById('cors-warning').style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
// Execute dynamic WebGIS loader upon page boot
|
||||
window.addEventListener('DOMContentLoaded', loadGeoJSONLayers);
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user