diff --git a/Choropleth/ptk.html b/Choropleth/index.html
similarity index 100%
rename from Choropleth/ptk.html
rename to Choropleth/index.html
diff --git a/Dockerfile b/Dockerfile
index 73cb7d4..f7425cb 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,6 +2,10 @@ FROM php:8.2-apache
RUN a2enmod rewrite
+# The sub-projects connect via mysqli, which isn't bundled in the base image
+RUN docker-php-ext-install mysqli pdo_mysql \
+ && echo "date.timezone = Asia/Jakarta" > /usr/local/etc/php/conf.d/timezone.ini
+
COPY . /var/www/html/
RUN chown -R www-data:www-data /var/www/html \
diff --git a/Dockerfile.db b/Dockerfile.db
new file mode 100644
index 0000000..8c8a3d4
--- /dev/null
+++ b/Dockerfile.db
@@ -0,0 +1,9 @@
+FROM mysql:8.0
+
+# Bake all three sub-project schemas + seed data into one database.
+# No table-name collisions across them, and config.php reads the DB name
+# from the DB_NAME env var, so a single shared database is fine.
+# Files in initdb.d run alphabetically against $MYSQL_DATABASE on first boot.
+COPY spbu/webgis.sql /docker-entrypoint-initdb.d/01_spbu.sql
+COPY parsiljalan/webgis2.sql /docker-entrypoint-initdb.d/02_jalantanah.sql
+COPY povertymap/gis_penduduk_miskin.sql /docker-entrypoint-initdb.d/03_miskin.sql
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..0b1cf90
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,60 @@
+name: muthia-webgis
+
+services:
+ app:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ image: muthia-webgis-app
+ container_name: muthia-webgis-app
+ ports:
+ - "127.0.0.1:8089:80"
+ environment:
+ DB_HOST: db
+ DB_USER: webgis_user
+ DB_PASS: webgis_password
+ DB_NAME: webgis_muthia
+ depends_on:
+ db:
+ condition: service_healthy
+ restart: unless-stopped
+
+ db:
+ build:
+ context: .
+ dockerfile: Dockerfile.db
+ image: muthia-webgis-db
+ container_name: muthia-webgis-db
+ environment:
+ MYSQL_DATABASE: webgis_muthia
+ MYSQL_USER: webgis_user
+ MYSQL_PASSWORD: webgis_password
+ MYSQL_ROOT_PASSWORD: root_password
+ expose:
+ - "3306"
+ volumes:
+ - db_data:/var/lib/mysql
+ healthcheck:
+ test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent"]
+ interval: 10s
+ timeout: 5s
+ retries: 10
+ restart: unless-stopped
+
+ phpmyadmin:
+ image: phpmyadmin:latest
+ container_name: muthia-webgis-phpmyadmin
+ ports:
+ - "127.0.0.1:8090:80"
+ environment:
+ PMA_HOST: db
+ PMA_PORT: 3306
+ PMA_ABSOLUTE_URI: https://muthia.webgis-untan.xyz/db/
+ UPLOAD_LIMIT: 64M
+ depends_on:
+ db:
+ condition: service_healthy
+ restart: unless-stopped
+
+volumes:
+ db_data:
diff --git a/index.php b/index.php
index 824516a..93e16bb 100644
--- a/index.php
+++ b/index.php
@@ -1,9 +1,9 @@
diff --git a/SIG2_jalantanah/Dockerfile b/parsiljalan/Dockerfile
similarity index 100%
rename from SIG2_jalantanah/Dockerfile
rename to parsiljalan/Dockerfile
diff --git a/SIG2_jalantanah/api.php b/parsiljalan/api.php
similarity index 84%
rename from SIG2_jalantanah/api.php
rename to parsiljalan/api.php
index 4c48c40..86a92a7 100644
--- a/SIG2_jalantanah/api.php
+++ b/parsiljalan/api.php
@@ -15,12 +15,58 @@ function toGeoJsonPoint($lat, $lng) {
return json_encode(['type' => 'Point', 'coordinates' => [(float)$lng, (float)$lat]]);
}
+// Deteksi POST yang melebihi post_max_size: PHP mengosongkan $_POST/$_FILES
+// padahal CONTENT_LENGTH > 0. Tanpa ini respons jadi kosong -> "Koneksi gagal!".
+if ($_SERVER['REQUEST_METHOD'] === 'POST'
+ && empty($_POST) && empty($_FILES)
+ && (int)($_SERVER['CONTENT_LENGTH'] ?? 0) > 0) {
+ header('Content-Type: application/json; charset=utf-8');
+ http_response_code(413);
+ echo json_encode([
+ 'ok' => false,
+ 'msg' => 'Data terlalu besar (melebihi post_max_size=' . ini_get('post_max_size')
+ . '). Kecilkan ukuran foto atau naikkan post_max_size.'
+ ]);
+ exit;
+}
+
$action = $_GET['action'] ?? $_POST['action'] ?? null;
if (!$action) return;
header('Content-Type: application/json; charset=utf-8');
$a = $action;
+if ($a === 'reverse') {
+ $lat = (float)($_GET['lat'] ?? 0);
+ $lon = (float)($_GET['lon'] ?? $_GET['lng'] ?? 0);
+ if ($lat == 0 && $lon == 0) { echo json_encode(['error' => 'koordinat kosong']); exit; }
+ $url = 'https://nominatim.openstreetmap.org/reverse?format=jsonv2&zoom=17&accept-language=id'
+ . '&lat=' . urlencode($lat) . '&lon=' . urlencode($lon);
+ // Nominatim mewajibkan User-Agent yang valid; browser tak bisa set ini, jadi diproxy dari server
+ $ua = 'SIG2-JalanTanah/1.0 (kontak: webmaster@webgis-untan.xyz)';
+ $body = false;
+ if (function_exists('curl_init')) {
+ $ch = curl_init($url);
+ curl_setopt_array($ch, [
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_USERAGENT => $ua,
+ CURLOPT_TIMEOUT => 8,
+ CURLOPT_FOLLOWLOCATION => true,
+ ]);
+ $body = curl_exec($ch);
+ curl_close($ch);
+ }
+ if ($body === false) {
+ $ctx = stream_context_create(['http' => [
+ 'header' => "User-Agent: $ua\r\n",
+ 'timeout' => 8,
+ ]]);
+ $body = @file_get_contents($url, false, $ctx);
+ }
+ if ($body === false) { echo json_encode(['error' => 'gagal hubungi nominatim']); exit; }
+ echo $body; exit;
+}
+
if ($a === 'search') {
$q = '%' . $conn->real_escape_string($_GET['q'] ?? '') . '%';
$tipe = $_GET['tipe'] ?? 'semua';
diff --git a/SIG2_jalantanah/assets/jalan-input.js b/parsiljalan/assets/jalan-input.js
similarity index 85%
rename from SIG2_jalantanah/assets/jalan-input.js
rename to parsiljalan/assets/jalan-input.js
index 40a7c8f..0ad640f 100644
--- a/SIG2_jalantanah/assets/jalan-input.js
+++ b/parsiljalan/assets/jalan-input.js
@@ -250,11 +250,12 @@ function openRusakModal(latlng) {
document.getElementById('rusak-ket').value = '';
clearFoto();
- fetch('https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=' + latlng.lat + '&lon=' + latlng.lng + '&zoom=17&accept-language=id')
+ fetch('api.php?action=reverse&lat=' + latlng.lat + '&lon=' + latlng.lng)
.then(function (r) { return r.json(); })
.then(function (geo) {
- var road = geo.address && (geo.address.road || geo.address.residential || geo.address.path || geo.address.pedestrian || geo.address.footway || '');
- document.getElementById('rusak-nama-jalan').value = road || '(Jalan tidak teridentifikasi)';
+ var a = geo.address || {};
+ var road = a.road || a.residential || a.path || a.pedestrian || a.footway || a.track || a.cycleway || a.neighbourhood || a.hamlet || a.village || a.suburb || '';
+ document.getElementById('rusak-nama-jalan').value = road || geo.display_name || '(Jalan tidak teridentifikasi)';
})
.catch(function () {
document.getElementById('rusak-nama-jalan').value = '(Gagal deteksi nama jalan)';
@@ -327,10 +328,11 @@ function handleFoto(input) {
badge.textContent = 'GPS dari Foto';
}
- fetch('https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=' + latDec + '&lon=' + lonDec + '&zoom=17&accept-language=id')
+ fetch('api.php?action=reverse&lat=' + latDec + '&lon=' + lonDec)
.then(function (r) { return r.json(); })
.then(function (geo) {
- var road = geo.address && (geo.address.road || geo.address.residential || geo.address.path || geo.address.pedestrian || geo.address.footway || '');
+ var a = geo.address || {};
+ var road = a.road || a.residential || a.path || a.pedestrian || a.footway || a.track || a.cycleway || a.neighbourhood || a.hamlet || a.village || a.suburb || '';
document.getElementById('rusak-nama-jalan').value = road || geo.display_name || '(Jalan tidak teridentifikasi)';
});
}
@@ -339,11 +341,31 @@ function handleFoto(input) {
var reader = new FileReader();
reader.onload = function (e) {
- fotoBase64 = e.target.result;
- var preview = document.getElementById('photo-preview');
- preview.src = fotoBase64;
- preview.style.display = 'block';
- document.getElementById('foto-clear-btn').style.display = 'flex';
+ // Kompres & resize foto agar payload kecil (hindari post_max_size & batas kolom DB)
+ var img = new Image();
+ img.onload = function () {
+ var maxDim = 1280;
+ var w = img.width, h = img.height;
+ if (w > h && w > maxDim) { h = Math.round(h * maxDim / w); w = maxDim; }
+ else if (h >= w && h > maxDim) { w = Math.round(w * maxDim / h); h = maxDim; }
+ var canvas = document.createElement('canvas');
+ canvas.width = w; canvas.height = h;
+ canvas.getContext('2d').drawImage(img, 0, 0, w, h);
+ fotoBase64 = canvas.toDataURL('image/jpeg', 0.7);
+ var preview = document.getElementById('photo-preview');
+ preview.src = fotoBase64;
+ preview.style.display = 'block';
+ document.getElementById('foto-clear-btn').style.display = 'flex';
+ };
+ img.onerror = function () {
+ // Fallback bila gambar tak bisa di-decode (mis. format tak didukung canvas)
+ fotoBase64 = e.target.result;
+ var preview = document.getElementById('photo-preview');
+ preview.src = fotoBase64;
+ preview.style.display = 'block';
+ document.getElementById('foto-clear-btn').style.display = 'flex';
+ };
+ img.src = e.target.result;
};
reader.readAsDataURL(file);
}
@@ -381,27 +403,40 @@ function saveRusak() {
formData.append('foto', fotoBase64);
fetch('api.php', { method: 'POST', body: formData })
- .then(function (r) { return r.json(); })
+ .then(function (r) {
+ return r.text().then(function (txt) {
+ var res;
+ try { res = JSON.parse(txt); }
+ catch (e) {
+ // Respons bukan JSON: tampilkan status & potongan isi supaya jelas penyebabnya
+ var info = txt.trim() ? txt.trim().slice(0, 200) : '(respons kosong)';
+ throw new Error('HTTP ' + r.status + ' — ' + info);
+ }
+ return res;
+ });
+ })
.then(function (res) {
btn.textContent = 'Simpan Titik';
btn.disabled = false;
if (res.ok) {
showToast('Titik jalan rusak disimpan!', 'success');
+ var savedFoto = fotoBase64; // simpan dulu: closeRusakModal() me-reset fotoBase64
closeRusakModal();
var id = res.id;
var nama = res.nama || pelapor;
- var layer = makeRusakLayer(id, nama, tanggal, lat, lng, ket, fotoBase64, 'recent', false);
+ var layer = makeRusakLayer(id, nama, tanggal, lat, lng, ket, savedFoto, 'recent', false);
rusakLayers[id] = layer;
- rusakData[id] = { id: id, nama: nama, tgl: tanggal, lat: lat, lng: lng, ket: ket, foto: fotoBase64, age: 'recent', recurring: false };
+ rusakData[id] = { id: id, nama: nama, tgl: tanggal, lat: lat, lng: lng, ket: ket, foto: savedFoto, age: 'recent', recurring: false };
if (res.tr !== undefined) updateStats(res);
cancelDraw();
} else {
showToast('Gagal: ' + (res.msg || 'unknown'), 'error');
}
})
- .catch(function () {
+ .catch(function (err) {
btn.textContent = 'Simpan Titik';
btn.disabled = false;
- showToast('Koneksi gagal!', 'error');
+ showToast('Gagal simpan: ' + (err && err.message ? err.message : 'koneksi gagal'), 'error');
+ console.error('saveRusak error:', err);
});
}
diff --git a/SIG2_jalantanah/assets/jalan.css b/parsiljalan/assets/jalan.css
similarity index 100%
rename from SIG2_jalantanah/assets/jalan.css
rename to parsiljalan/assets/jalan.css
diff --git a/SIG2_jalantanah/config.php b/parsiljalan/config.php
similarity index 100%
rename from SIG2_jalantanah/config.php
rename to parsiljalan/config.php
diff --git a/SIG2_jalantanah/db2.php b/parsiljalan/db2.php
similarity index 100%
rename from SIG2_jalantanah/db2.php
rename to parsiljalan/db2.php
diff --git a/SIG2_jalantanah/includes/confirm-modal.php b/parsiljalan/includes/confirm-modal.php
similarity index 100%
rename from SIG2_jalantanah/includes/confirm-modal.php
rename to parsiljalan/includes/confirm-modal.php
diff --git a/SIG2_jalantanah/includes/stats.php b/parsiljalan/includes/stats.php
similarity index 100%
rename from SIG2_jalantanah/includes/stats.php
rename to parsiljalan/includes/stats.php
diff --git a/SIG2_jalantanah/index.php b/parsiljalan/index.php
similarity index 99%
rename from SIG2_jalantanah/index.php
rename to parsiljalan/index.php
index 268866d..4af198f 100644
--- a/SIG2_jalantanah/index.php
+++ b/parsiljalan/index.php
@@ -1080,7 +1080,7 @@ function exitFloatingGeoEdit(save) {
}
}
-function spHandleFoto(input){var f=input.files[0];if(!f)return;if(f.size>5*1024*1024){showToast('Foto maks 5MB!','error');return;}var r=new FileReader();r.onload=function(e){spFotoBase64=e.target.result;el('sp-foto-preview').src=spFotoBase64;el('sp-foto-preview').style.display='block';el('sp-foto-clear').style.display='flex';};r.readAsDataURL(f);}
+function spHandleFoto(input){var f=input.files[0];if(!f)return;if(f.size>5*1024*1024){showToast('Foto maks 5MB!','error');return;}var r=new FileReader();r.onload=function(e){var img=new Image();img.onload=function(){var maxDim=1280,w=img.width,h=img.height;if(w>h&&w>maxDim){h=Math.round(h*maxDim/w);w=maxDim;}else if(h>=w&&h>maxDim){w=Math.round(w*maxDim/h);h=maxDim;}var c=document.createElement('canvas');c.width=w;c.height=h;c.getContext('2d').drawImage(img,0,0,w,h);spFotoBase64=c.toDataURL('image/jpeg',0.7);el('sp-foto-preview').src=spFotoBase64;el('sp-foto-preview').style.display='block';el('sp-foto-clear').style.display='flex';};img.onerror=function(){spFotoBase64=e.target.result;el('sp-foto-preview').src=spFotoBase64;el('sp-foto-preview').style.display='block';el('sp-foto-clear').style.display='flex';};img.src=e.target.result;};r.readAsDataURL(f);}
function spClearFoto(){spFotoBase64='';el('sp-foto-preview').style.display='none';el('sp-foto-file').value='';el('sp-foto-clear').style.display='none';}
/* ══ GEOMETRY EDIT ═══════════════════════════════════ */
@@ -1410,7 +1410,7 @@ if (document.getElementById('lnav-list')) {
}
-
+