Meta Description" name="description" />
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<title>GIS Mobile Pro - MVT Masterpiece</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css" rel="stylesheet" />
<script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script>
<script src="https://unpkg.com/pmtiles@2.11.0/dist/index.js"></script>
<script src="https://unpkg.com/@turf/turf@6/turf.min.js"></script>
<script src="https://unpkg.com/shpjs@latest/dist/shp.min.js"></script>
<style>
:root { --primary: #1e88e5; --dark: #111; --panel-bg: rgba(26, 26, 26, 0.98); }
html,body,#map{height:100%;margin:0;font-family: 'Segoe UI', sans-serif; background:var(--dark); overflow:hidden;}
/* PUSAT PETA (CROSSHAIR) */
.map-reticle {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
z-index: 1000; pointer-events: none; color: white; font-size: 24px;
text-shadow: 0 0 4px #000; opacity: 0.8;
}
/* MENU TOGGLE */
.menu-toggle {
position:absolute; top:15px; left:15px; z-index:1200;
background:var(--primary); color:white; padding:12px 20px; border-radius:10px;
font-weight:bold; cursor:pointer; box-shadow:0 4px 15px rgba(0,0,0,0.5);
}
/* PANEL UTAMA */
.panel {
position:absolute; top:0; left:-330px; width:310px; height:100%;
background:var(--panel-bg); color:white; padding:80px 15px 20px;
transition:0.3s ease; z-index:1150; overflow-y:auto;
border-right: 1px solid rgba(255,255,255,0.1);
}
.panel.active { left:0; }
.tool-section { margin-bottom: 20px; border-bottom: 1px solid #333; padding-bottom: 15px; }
.tool-label { font-size: 10px; text-transform: uppercase; color: #777; letter-spacing: 1px; margin-bottom: 10px; display: block; }
/* TOMBOL TOOL */
.btn-tool {
width: 100%; padding: 12px; margin-bottom: 6px; border: none; border-radius: 8px;
background: rgba(255,255,255,0.05); color: #eee; text-align: left; cursor: pointer;
font-size: 13px; display: flex; align-items: center; gap: 10px; transition: 0.2s;
}
.btn-tool:hover { background: rgba(255,255,255,0.1); }
.btn-tool:active { background: var(--primary); transform: scale(0.98); }
/* UI KONTROL PLOT & UKUR */
#drawControls {
position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%);
display: none; background: white; padding: 10px; border-radius: 50px;
box-shadow: 0 10px 30px rgba(0,0,0,0.5); z-index: 1300;
}
.btn-draw { padding: 12px 22px; border-radius: 30px; border: none; font-weight: bold; cursor: pointer; margin: 0 5px; }
.btn-plot { background: var(--primary); color: white; }
.btn-done { background: #2e7d32; color: white; }
/* INFO UKURAN */
#measureInfo {
position: absolute; top: 15px; left: 50%; transform: translateX(-50%);
background: rgba(0,0,0,0.8); color: #00e676; padding: 10px 20px;
border-radius: 8px; font-family: monospace; z-index: 1100; display: none;
border: 1px solid #00e676; text-align: center;
}
</style>
</head>
<body>
<div class="map-reticle">+</div>
<div id="measureInfo">Unit: 0.00</div>
<div id="map"></div>
<div class="menu-toggle" id="menuBtn">β° DASHBOARD</div>
<div class="panel" id="sidePanel">
<div class="tool-section">
<span class="tool-label">Navigation</span>
<button class="btn-tool" id="btnGPS">π Posisi Saya</button>
</div>
<div class="tool-section">
<span class="tool-label">Survey Tools</span>
<button class="btn-tool" id="btnStartPlot">π Plotting Titik</button>
<button class="btn-tool" id="btnStartPoly">β¬’ Buat Polygon</button>
<button class="btn-tool" id="btnStartTrack">π§ Start Tracking</button>
</div>
<div class="tool-section">
<span class="tool-label">Measurement</span>
<button class="btn-tool" id="btnMeasureLine">π Ukur Garis</button>
<button class="btn-tool" id="btnMeasureArea">π Ukur Luas Polygon</button>
</div>
<div class="tool-section">
<span class="tool-label">Advanced & Data</span>
<button class="btn-tool" id="btnImportSHP">π¦ Import SHP (ZIP)</button>
<button class="btn-tool" id="btnExportKML">β¬ Export Plotting KML</button>
<button class="btn-tool" id="btnAttrTable" style="color: #ffca28;">π Tabel Atribut</button>
</div>
<div class="tool-section">
<span class="tool-label">Gallery Plotting</span>
<div id="galleryContainer" style="font-size: 12px; color: #aaa;">Belum ada data...</div>
</div>
</div>
<div id="drawControls">
<button class="btn-draw btn-plot" id="btnConfirmAction">β PLOT</button>
<button class="btn-draw btn-done" id="btnFinishAction">β
SELESAI</button>
<button class="btn-draw" id="btnCancelAction" style="background:#f5f5f5">β</button>
</div>
<script>
/* --- 1. INISIALISASI ENGINE PETA --- */
const map = new maplibregl.Map({
container: 'map',
style: {
'version': 8,
'sources': {
'satellite': {
'type': 'raster',
'tiles': ['https://mt1.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}'],
'tileSize': 256
}
},
'layers': [{'id': 'satellite', 'type': 'raster', 'source': 'satellite'}]
},
center: [118, -2.5],
zoom: 5
});
/* --- 2. STATE & DATA MANAGEMENT --- */
let activeMode = null;
let currentCoords = [];
let plotGallery = [];
let trackWatchId = null;
// Struktur GeoJSON Awal
let surveyGeoJSON = {
'type': 'FeatureCollection',
'features': []
};
/* --- 3. SETUP LAYER DATA (LOAD) --- */
map.on('load', () => {
map.addSource('survey-data', {
'type': 'geojson',
'data': surveyGeoJSON
});
// Layer Poligon (Fill)
map.addLayer({
'id': 'survey-fill', 'type': 'fill', 'source': 'survey-data',
'filter': ['==', '$type', 'Polygon'],
'paint': { 'fill-color': '#1e88e5', 'fill-opacity': 0.4 }
});
// Layer Garis (Outline/Tracking)
map.addLayer({
'id': 'survey-line', 'type': 'line', 'source': 'survey-data',
'filter': ['any', ['==', '$type', 'LineString'], ['==', '$type', 'Polygon']],
'paint': { 'line-color': '#ffffff', 'line-width': 2.5 }
});
// Layer Titik (Plotting)
map.addLayer({
'id': 'survey-point', 'type': 'circle', 'source': 'survey-data',
'filter': ['==', '$type', 'Point'],
'paint': {
'circle-radius': 7, 'circle-color': '#ff5252',
'circle-stroke-width': 2, 'circle-stroke-color': '#fff'
}
});
});
/* --- 4. UI INTERACTION HANDLER --- */
const panel = document.getElementById('sidePanel');
const menuBtn = document.getElementById('menuBtn');
menuBtn.addEventListener('click', () => panel.classList.toggle('active'));
function closePanel() { panel.classList.remove('active'); }
// Mencegah interaksi peta saat menyentuh UI
const preventMapDrag = (id) => {
const el = document.getElementById(id);
if(el) {
['mousedown', 'touchstart', 'dblclick'].forEach(ev => {
el.addEventListener(ev, e => e.stopPropagation());
});
}
};
['sidePanel', 'drawControls'].forEach(preventMapDrag);
/* --- 5. FUNGSI NAVIGATION (GPS) --- */
document.getElementById('btnGPS').addEventListener('click', () => {
if (!navigator.geolocation) return alert("GPS Tidak Tersedia");
navigator.geolocation.getCurrentPosition(pos => {
const { longitude, latitude } = pos.coords;
map.flyTo({ center: [longitude, latitude], zoom: 18 });
new maplibregl.Marker({ color: "#2196f3" })
.setLngLat([longitude, latitude])
.addTo(map);
}, (err) => alert("Gagal mendapatkan lokasi: " + err.message),
{ enableHighAccuracy: true });
closePanel();
});
/* --- 6. CORE SURVEY LOGIC --- */
const startSurvey = (mode) => {
activeMode = mode;
currentCoords = [];
document.getElementById('drawControls').style.display = 'flex';
document.getElementById('measureInfo').style.display = (mode.includes('measure')) ? 'block' : 'none';
closePanel();
};
document.getElementById('btnStartPlot').addEventListener('click', () => startSurvey('plotting'));
document.getElementById('btnStartPoly').addEventListener('click', () => startSurvey('polygon'));
document.getElementById('btnMeasureLine').addEventListener('click', () => startSurvey('measure_line'));
document.getElementById('btnMeasureArea').addEventListener('click', () => startSurvey('measure_area'));
// Tombol Konfirmasi PLOT (+) di tengah layar
document.getElementById('btnConfirmAction').addEventListener('click', () => {
const center = map.getCenter();
const coord = [center.lng, center.lat];
if (activeMode === 'plotting') {
const name = prompt("Nama Titik:", "Titik " + (plotGallery.length + 1));
if (name) saveFeature(turf.point(coord, { name, id: Date.now() }));
exitMode();
} else {
currentCoords.push(coord);
updatePreview();
}
});
</script>
/* --- 7. LOGIKA PREVIEW & PENGUKURAN --- */
function updatePreview() {
if (currentCoords.length < 2) return;
let preview;
if (activeMode === 'polygon' || activeMode === 'measure_area') {
if (currentCoords.length > 2) {
preview = turf.polygon([[...currentCoords, currentCoords[0]]]);
if (activeMode === 'measure_area') updateMeasureLabel(turf.area(preview), 'area');
}
} else {
preview = turf.lineString(currentCoords);
if (activeMode.includes('measure')) updateMeasureLabel(turf.length(preview, {units: 'kilometers'}), 'length');
}
// Tampilkan preview di peta secara real-time
if (preview) {
const source = map.getSource('survey-data');
const tempFeatures = [...surveyGeoJSON.features, preview];
source.setData({ 'type': 'FeatureCollection', 'features': tempFeatures });
}
}
/* --- 8. TRACKING GPS --- */
document.getElementById('btnStartTrack').addEventListener('click', function() {
if (activeMode === 'tracking') {
stopTracking();
this.innerText = "π§ Start Tracking";
this.style.background = "";
} else {
activeMode = 'tracking';
currentCoords = [];
this.innerText = "π Stop Tracking";
this.style.background = "#d32f2f";
trackWatchId = navigator.geolocation.watchPosition(pos => {
const coord = [pos.coords.longitude, pos.coords.latitude];
currentCoords.push(coord);
map.setCenter(coord);
updatePreview();
}, null, { enableHighAccuracy: true });
}
closePanel();
});
function stopTracking() {
navigator.geolocation.clearWatch(trackWatchId);
if (currentCoords.length > 1) {
saveFeature(turf.lineString(currentCoords, { name: "Track " + new Date().toLocaleTimeString() }));
}
exitMode();
}
/* --- 9. PENGELOLAAN DATA (SAVE & GALLERY) --- */
function saveFeature(feature) {
surveyGeoJSON.features.push(feature);
map.getSource('survey-data').setData(surveyGeoJSON);
plotGallery.push(feature);
updateGallery();
}
function updateGallery() {
const container = document.getElementById('galleryContainer');
container.innerHTML = plotGallery.map(f => `
<div class="btn-tool" style="font-size:11px">
π¦ ${f.properties.name || 'Unnamed'}
</div>
`).join('');
}
function exitMode() {
activeMode = null;
currentCoords = [];
document.getElementById('drawControls').style.display = 'none';
document.getElementById('measureInfo').style.display = 'none';
map.getSource('survey-data').setData(surveyGeoJSON); // Reset view ke data tersimpan
}
document.getElementById('btnFinishAction').addEventListener('click', () => {
if (activeMode === 'polygon' && currentCoords.length > 2) {
const name = prompt("Nama Polygon:", "Area " + plotGallery.length);
saveFeature(turf.polygon([[...currentCoords, currentCoords[0]]], { name }));
}
exitMode();
});
document.getElementById('btnCancelAction').addEventListener('click', exitMode);
function updateMeasureLabel(val, type) {
const el = document.getElementById('measureInfo');
if (type === 'length') el.innerText = `Jarak: ${val.toFixed(3)} km | ${(val*1000).toFixed(0)} m`;
else el.innerText = `Luas: ${(val/10000).toFixed(2)} Ha | ${(val/1000000).toFixed(3)} kmΒ²`;
}
/* --- 10. ADVANCED SYMBOLOGY & UI COMPONENTS --- */
const symStyle = `
<style>
#symPanel {
position:fixed; top:0; right:-320px; width:300px; height:100%;
background:#1a1a1a; color:white; z-index:1500; transition:0.3s; padding:20px;
border-left:1px solid #333; overflow-y:auto;
}
#symPanel.active { right:0; }
.attr-wrap {
position:fixed; bottom:-50%; left:0; width:100%; height:50%;
background:#1a1a1a; z-index:1400; transition:0.3s; display:flex; flex-direction:column;
border-top: 2px solid var(--primary);
}
.attr-wrap.active { bottom:0; }
.attr-handle { height:30px; background:#333; display:flex; justify-content:center; align-items:center; cursor:grab; }
.table-scroll { overflow:auto; flex:1; }
table { width:100%; border-collapse:collapse; font-size:12px; }
th { background:#222; position:sticky; top:0; padding:10px; border:1px solid #444; }
td { padding:8px; border:1px solid #444; text-align:center; }
</style>`;
document.head.insertAdjacentHTML('beforeend', symStyle);
const symHTML = `
<div id="symPanel">
<span class="tool-label">Simbologi & Labeling</span>
<div style="margin-bottom:15px;">
<label><input type="checkbox" id="checkHollow"> Hollow (Tanpa Isian)</label>
</div>
<label>Warna Isian:</label>
<input type="color" id="colorFill" value="#1e88e5" style="width:100%; margin-bottom:10px;">
<label>Warna Garis:</label>
<input type="color" id="colorLine" value="#ffffff" style="width:100%; margin-bottom:10px;">
<label>Ketebalan Garis:</label>
<input type="range" id="widthLine" min="1" max="10" value="2" style="width:100%; margin-bottom:15px;">
<label>Pilih Field Label:</label>
<select id="labelFieldSelect" style="width:100%; padding:8px; background:#333; color:white; border:none; margin-bottom:15px;"><option value="">Tanpa Label</option></select>
<button class="btn-tool" style="background:var(--primary)" id="applySymBtn">TERAPKAN GAYA</button>
<button class="btn-tool" style="background:#444; margin-top:10px;" onclick="document.getElementById('symPanel').classList.remove('active')">TUTUP</button>
</div>
<div id="attrWrap" class="attr-wrap">
<div class="attr-handle" id="attrHandle">β TABEL ATRIBUT β</div>
<div class="table-scroll" id="attrTableBody"></div>
</div>`;
document.body.insertAdjacentHTML('beforeend', symHTML);
/* --- 11. IMPORT SHP & TABEL ATRIBUT --- */
document.getElementById('btnImportSHP').addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file'; input.accept = '.zip';
input.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const buffer = await file.arrayBuffer();
const geojson = await shp(buffer);
const features = Array.isArray(geojson) ? geojson[0].features : geojson.features;
surveyGeoJSON.features.push(...features);
map.getSource('survey-data').setData(surveyGeoJSON);
const fields = Object.keys(features[0].properties);
document.getElementById('labelFieldSelect').innerHTML = '<option value="">Tanpa Label</option>' +
fields.map(f => `<option value="${f}">${f}</option>`).join('');
const bbox = turf.bbox({type: 'FeatureCollection', features: features});
map.fitBounds([[bbox[0], bbox[1]], [bbox[2], bbox[3]]], {padding: 50});
buildAttributeTable(features);
alert("SHP Berhasil Dimuat!");
} catch (err) { alert("Error: Pastikan ZIP berisi file .shp, .dbf, dll."); }
};
input.click();
closePanel();
});
function buildAttributeTable(features) {
const container = document.getElementById('attrTableBody');
if (!features || features.length === 0) return;
let html = `<table><thead><tr>`;
const keys = Object.keys(features[0].properties);
keys.forEach(k => html += `<th>${k}</th>`);
html += `</tr></thead><tbody>`;
features.forEach(f => {
html += `<tr>`;
keys.forEach(k => html += `<td contenteditable="true">${f.properties[k] || ''}</td>`);
html += `</tr>`;
});
html += `</tbody></table>`;
container.innerHTML = html;
}
document.getElementById('btnAttrTable').addEventListener('click', () => {
document.getElementById('attrWrap').classList.toggle('active');
closePanel();
});
/* --- 12. APPLY SYMBOLOGY & EXPORT KML --- */
document.getElementById('applySymBtn').addEventListener('click', () => {
const hollow = document.getElementById('checkHollow').checked;
const fill = document.getElementById('colorFill').value;
const line = document.getElementById('colorLine').value;
const width = parseFloat(document.getElementById('widthLine').value);
const labelField = document.getElementById('labelFieldSelect').value;
map.setPaintProperty('survey-fill', 'fill-color', fill);
map.setPaintProperty('survey-fill', 'fill-opacity', hollow ? 0 : 0.4);
map.setPaintProperty('survey-line', 'line-color', line);
map.setPaintProperty('survey-line', 'line-width', width);
if (labelField) {
if (!map.getLayer('survey-labels')) {
map.addLayer({
'id': 'survey-labels', 'type': 'symbol', 'source': 'survey-data',
'layout': { 'text-field': ['get', labelField], 'text-size': 12, 'text-variable-anchor': ['top'], 'text-radial-offset': 0.5 },
'paint': { 'text-color': '#fff', 'text-halo-color': '#000', 'text-halo-width': 1 }
});
} else {
map.setLayoutProperty('survey-labels', 'text-field', ['get', labelField]);
}
}
});
document.getElementById('btnExportKML').addEventListener('click', () => {
if (surveyGeoJSON.features.length === 0) return alert("Belum ada data!");
let kml = `<?xml version="1.0" encoding="UTF-8"?><kml xmlns="http://www.opengis.net/kml/2.2"><Document>`;
surveyGeoJSON.features.forEach(f => {
kml += `<Placemark><name>${f.properties.name || 'Feature'}</name>`;
if (f.geometry.type === 'Point') {
kml += `<Point><coordinates>${f.geometry.coordinates.join(',')}</coordinates></Point>`;
} else if (f.geometry.type === 'Polygon') {
const coords = f.geometry.coordinates[0].map(c => c.join(',')).join(' ');
kml += `<Polygon><outerBoundaryIs><LinearRing><coordinates>${coords}</coordinates></LinearRing></outerBoundaryIs></Polygon>`;
} else if (f.geometry.type === 'LineString') {
const coords = f.geometry.coordinates.map(c => c.join(',')).join(' ');
kml += `<LineString><coordinates>${coords}</coordinates></LineString>`;
}
kml += `</Placemark>`;
});
kml += `</Document></kml>`;
const blob = new Blob([kml], {type: "application/vnd.google-earth.kml+xml"});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = "Survey_Export.kml";
a.click();
});
</script>
</body>
</html>
49
4
906KB
2042KB
5,301.0ms
3,492.0ms
5,393.0ms