Initial commit of antigravity2 project

This commit is contained in:
Danial HB
2026-06-11 22:52:58 +07:00
commit de35383342
3016 changed files with 484480 additions and 0 deletions
@@ -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: '&copy; <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>