480 lines
14 KiB
PHP
480 lines
14 KiB
PHP
<?php
|
|
header('Content-Type: application/json');
|
|
require_once 'config.php';
|
|
|
|
function decodeGeometry($rawGeometry) {
|
|
if (is_array($rawGeometry)) {
|
|
return $rawGeometry;
|
|
}
|
|
$decoded = json_decode($rawGeometry, true);
|
|
return is_array($decoded) ? $decoded : null;
|
|
}
|
|
|
|
function geometryRings($geometry) {
|
|
if (!is_array($geometry) || !isset($geometry['type']) || !isset($geometry['coordinates'])) {
|
|
return [];
|
|
}
|
|
|
|
if ($geometry['type'] === 'Polygon') {
|
|
return [$geometry['coordinates']];
|
|
}
|
|
|
|
if ($geometry['type'] === 'MultiPolygon') {
|
|
return $geometry['coordinates'];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
function projectLonLatToMeters($lng, $lat, $referenceLat) {
|
|
$earthRadius = 6371000.0;
|
|
$x = deg2rad((float)$lng) * $earthRadius * cos(deg2rad((float)$referenceLat));
|
|
$y = deg2rad((float)$lat) * $earthRadius;
|
|
return [$x, $y];
|
|
}
|
|
|
|
function ringAreaSquareMeters($ring) {
|
|
if (!is_array($ring) || count($ring) < 4) {
|
|
return 0.0;
|
|
}
|
|
|
|
$sumLat = 0.0;
|
|
$count = 0;
|
|
foreach ($ring as $point) {
|
|
if (is_array($point) && count($point) >= 2) {
|
|
$sumLat += (float)$point[1];
|
|
$count++;
|
|
}
|
|
}
|
|
|
|
if ($count === 0) {
|
|
return 0.0;
|
|
}
|
|
|
|
$referenceLat = $sumLat / $count;
|
|
$projected = [];
|
|
foreach ($ring as $point) {
|
|
if (is_array($point) && count($point) >= 2) {
|
|
$projected[] = projectLonLatToMeters((float)$point[0], (float)$point[1], $referenceLat);
|
|
}
|
|
}
|
|
|
|
if (count($projected) < 4) {
|
|
return 0.0;
|
|
}
|
|
|
|
$area = 0.0;
|
|
$maxIndex = count($projected) - 1;
|
|
for ($i = 0; $i < $maxIndex; $i++) {
|
|
$area += ($projected[$i][0] * $projected[$i + 1][1]) - ($projected[$i + 1][0] * $projected[$i][1]);
|
|
}
|
|
|
|
return abs($area / 2.0);
|
|
}
|
|
|
|
function geometryAreaKm2($geometry) {
|
|
$polygons = geometryRings($geometry);
|
|
if (empty($polygons)) {
|
|
return 0.0;
|
|
}
|
|
|
|
$totalSquareMeters = 0.0;
|
|
|
|
foreach ($polygons as $polygonRings) {
|
|
if (!is_array($polygonRings) || empty($polygonRings)) {
|
|
continue;
|
|
}
|
|
|
|
$outerArea = ringAreaSquareMeters($polygonRings[0]);
|
|
$holeArea = 0.0;
|
|
|
|
for ($i = 1; $i < count($polygonRings); $i++) {
|
|
$holeArea += ringAreaSquareMeters($polygonRings[$i]);
|
|
}
|
|
|
|
$totalSquareMeters += max($outerArea - $holeArea, 0.0);
|
|
}
|
|
|
|
return $totalSquareMeters / 1000000.0;
|
|
}
|
|
|
|
function pointInRing($lng, $lat, $ring) {
|
|
if (!is_array($ring) || count($ring) < 4) {
|
|
return false;
|
|
}
|
|
|
|
$inside = false;
|
|
$lastIndex = count($ring) - 1;
|
|
|
|
for ($i = 0, $j = $lastIndex; $i <= $lastIndex; $j = $i++) {
|
|
$xi = (float)$ring[$i][0];
|
|
$yi = (float)$ring[$i][1];
|
|
$xj = (float)$ring[$j][0];
|
|
$yj = (float)$ring[$j][1];
|
|
|
|
$intersect = (($yi > $lat) !== ($yj > $lat)) &&
|
|
($lng < ($xj - $xi) * ($lat - $yi) / (($yj - $yi) ?: 1e-12) + $xi);
|
|
|
|
if ($intersect) {
|
|
$inside = !$inside;
|
|
}
|
|
}
|
|
|
|
return $inside;
|
|
}
|
|
|
|
function pointInGeometry($lng, $lat, $geometry) {
|
|
$polygons = geometryRings($geometry);
|
|
if (empty($polygons)) {
|
|
return false;
|
|
}
|
|
|
|
foreach ($polygons as $polygonRings) {
|
|
if (!is_array($polygonRings) || empty($polygonRings)) {
|
|
continue;
|
|
}
|
|
|
|
if (!pointInRing($lng, $lat, $polygonRings[0])) {
|
|
continue;
|
|
}
|
|
|
|
$insideHole = false;
|
|
for ($i = 1; $i < count($polygonRings); $i++) {
|
|
if (pointInRing($lng, $lat, $polygonRings[$i])) {
|
|
$insideHole = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$insideHole) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function geometryCentroid($geometry) {
|
|
$polygons = geometryRings($geometry);
|
|
$sumLng = 0.0;
|
|
$sumLat = 0.0;
|
|
$count = 0;
|
|
|
|
foreach ($polygons as $polygonRings) {
|
|
if (!is_array($polygonRings) || empty($polygonRings) || !is_array($polygonRings[0])) {
|
|
continue;
|
|
}
|
|
|
|
foreach ($polygonRings[0] as $point) {
|
|
if (is_array($point) && count($point) >= 2) {
|
|
$sumLng += (float)$point[0];
|
|
$sumLat += (float)$point[1];
|
|
$count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($count === 0) {
|
|
return ['lng' => 109.33, 'lat' => -0.02];
|
|
}
|
|
|
|
return [
|
|
'lng' => $sumLng / $count,
|
|
'lat' => $sumLat / $count
|
|
];
|
|
}
|
|
|
|
function haversineMeters($lat1, $lng1, $lat2, $lng2) {
|
|
$earthRadius = 6371000.0;
|
|
$dLat = deg2rad((float)$lat2 - (float)$lat1);
|
|
$dLng = deg2rad((float)$lng2 - (float)$lng1);
|
|
|
|
$a = sin($dLat / 2) * sin($dLat / 2)
|
|
+ cos(deg2rad((float)$lat1)) * cos(deg2rad((float)$lat2))
|
|
* sin($dLng / 2) * sin($dLng / 2);
|
|
|
|
$c = 2 * atan2(sqrt($a), sqrt(max(1 - $a, 0.0)));
|
|
return $earthRadius * $c;
|
|
}
|
|
|
|
function roadCentroidLngLat($coordinates) {
|
|
if (!is_array($coordinates) || empty($coordinates)) {
|
|
return null;
|
|
}
|
|
|
|
$sumLat = 0.0;
|
|
$sumLng = 0.0;
|
|
$count = 0;
|
|
|
|
foreach ($coordinates as $pair) {
|
|
if (is_array($pair) && count($pair) >= 2 && is_numeric($pair[0]) && is_numeric($pair[1])) {
|
|
// Road coordinates are stored as [lat, lng]
|
|
$sumLat += (float)$pair[0];
|
|
$sumLng += (float)$pair[1];
|
|
$count++;
|
|
}
|
|
}
|
|
|
|
if ($count === 0) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'lng' => $sumLng / $count,
|
|
'lat' => $sumLat / $count
|
|
];
|
|
}
|
|
|
|
function deterministicRoadDensity($populationDensity, $areaId) {
|
|
$seed = ($areaId * 1103515245 + 12345) & 0x7fffffff;
|
|
$variation = 0.8 + (($seed % 1000) / 1000.0) * 0.6;
|
|
$base = 0.9 + (log(max($populationDensity, 1.0) + 1.0) / log(10)) * 1.4;
|
|
|
|
return round($base * $variation, 6);
|
|
}
|
|
|
|
function deterministicDamagedDensity($roadDensity, $areaId) {
|
|
$seed = ($areaId * 214013 + 2531011) & 0x7fffffff;
|
|
$ratio = 0.14 + (($seed % 1000) / 1000.0) * 0.18;
|
|
return round($roadDensity * $ratio, 6);
|
|
}
|
|
|
|
try {
|
|
$db = getDB();
|
|
|
|
// Get layer type parameter
|
|
$layer_type = isset($_GET['layer_type']) ? $_GET['layer_type'] : 'population';
|
|
$admin_level = isset($_GET['admin_level']) ? strtolower($_GET['admin_level']) : 'kecamatan';
|
|
|
|
// Validate layer type
|
|
$valid_layers = ['population', 'road_density', 'damaged_roads_density', 'city_boundary'];
|
|
if (!in_array($layer_type, $valid_layers)) {
|
|
throw new Exception('Invalid layer type. Must be: ' . implode(', ', $valid_layers));
|
|
}
|
|
|
|
// Special handling for city boundary
|
|
if ($layer_type === 'city_boundary') {
|
|
$stmt = $db->prepare('SELECT id, name, geometry FROM pontianak_areas WHERE area_type = ?');
|
|
$stmt->execute(['city']);
|
|
$cities = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$data_items = [];
|
|
foreach ($cities as $city) {
|
|
$data_items[] = [
|
|
'area_id' => $city['id'],
|
|
'area_name' => $city['name'],
|
|
'value' => 0, // No numeric value for boundary
|
|
'geometry' => json_decode($city['geometry'], true)
|
|
];
|
|
}
|
|
|
|
http_response_code(200);
|
|
echo json_encode([
|
|
'success' => true,
|
|
'layer_type' => 'city_boundary',
|
|
'data' => $data_items,
|
|
'min_value' => 0.0,
|
|
'max_value' => 1.0,
|
|
'admin_level' => 'city',
|
|
'value_source' => 'boundary'
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
$valid_admin_levels = ['kecamatan', 'kelurahan'];
|
|
if (!in_array($admin_level, $valid_admin_levels)) {
|
|
throw new Exception('Invalid admin_level. Must be: ' . implode(', ', $valid_admin_levels));
|
|
}
|
|
|
|
// Get Pontianak areas by selected admin level
|
|
$stmt = $db->prepare('SELECT id, name, geometry, population FROM pontianak_areas WHERE area_type = ? ORDER BY name');
|
|
$stmt->execute([$admin_level]);
|
|
$areas = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
if (empty($areas)) {
|
|
// Fallback to kecamatan when kelurahan data does not exist.
|
|
if ($admin_level === 'kelurahan') {
|
|
$stmt->execute(['kecamatan']);
|
|
$areas = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
$admin_level = 'kecamatan';
|
|
}
|
|
}
|
|
|
|
if (empty($areas)) {
|
|
throw new Exception('No Pontianak areas found for selected admin level. Run seed_pontianak_data.php?force=true first.');
|
|
}
|
|
|
|
$areaMetrics = [];
|
|
foreach ($areas as $area) {
|
|
$geometry = decodeGeometry($area['geometry']);
|
|
if ($geometry === null) {
|
|
continue;
|
|
}
|
|
|
|
$areaKm2 = geometryAreaKm2($geometry);
|
|
if ($areaKm2 <= 0) {
|
|
$areaKm2 = 0.01;
|
|
}
|
|
|
|
$population = (float)$area['population'];
|
|
$populationDensity = $population / $areaKm2;
|
|
|
|
$areaMetrics[(int)$area['id']] = [
|
|
'id' => (int)$area['id'],
|
|
'name' => $area['name'],
|
|
'geometry' => $geometry,
|
|
'area_km2' => $areaKm2,
|
|
'centroid' => geometryCentroid($geometry),
|
|
'population' => $population,
|
|
'population_density' => $populationDensity
|
|
];
|
|
}
|
|
|
|
if (empty($areaMetrics)) {
|
|
throw new Exception('Area geometry data is invalid for selected admin level.');
|
|
}
|
|
|
|
$areaRoadCounts = [];
|
|
$areaDamagedRoadCounts = [];
|
|
foreach ($areaMetrics as $metric) {
|
|
$areaRoadCounts[$metric['id']] = 0;
|
|
$areaDamagedRoadCounts[$metric['id']] = 0;
|
|
}
|
|
|
|
$roadsExist = false;
|
|
$valueSource = 'computed';
|
|
|
|
if ($layer_type === 'road_density' || $layer_type === 'damaged_roads_density') {
|
|
$roadStmt = $db->query('SELECT id, coordinates, condition_status FROM roads');
|
|
$roads = $roadStmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
foreach ($roads as $road) {
|
|
$coordinates = json_decode($road['coordinates'], true);
|
|
$centroid = roadCentroidLngLat($coordinates);
|
|
if ($centroid === null) {
|
|
continue;
|
|
}
|
|
|
|
$roadsExist = true;
|
|
|
|
$assignedAreaId = null;
|
|
foreach ($areaMetrics as $areaMetric) {
|
|
if (pointInGeometry($centroid['lng'], $centroid['lat'], $areaMetric['geometry'])) {
|
|
$assignedAreaId = $areaMetric['id'];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($assignedAreaId === null) {
|
|
$nearestDistance = null;
|
|
foreach ($areaMetrics as $areaMetric) {
|
|
$distance = haversineMeters(
|
|
$centroid['lat'],
|
|
$centroid['lng'],
|
|
$areaMetric['centroid']['lat'],
|
|
$areaMetric['centroid']['lng']
|
|
);
|
|
|
|
if ($nearestDistance === null || $distance < $nearestDistance) {
|
|
$nearestDistance = $distance;
|
|
$assignedAreaId = $areaMetric['id'];
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($assignedAreaId !== null) {
|
|
$areaRoadCounts[$assignedAreaId]++;
|
|
if (in_array($road['condition_status'], ['minor_damage', 'major_damage'])) {
|
|
$areaDamagedRoadCounts[$assignedAreaId]++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$roadsExist) {
|
|
$valueSource = 'dummy';
|
|
}
|
|
}
|
|
|
|
$data_items = [];
|
|
$values = [];
|
|
|
|
foreach ($areaMetrics as $areaMetric) {
|
|
$area_id = $areaMetric['id'];
|
|
$geometry = $areaMetric['geometry'];
|
|
$value = 0;
|
|
|
|
// Calculate value based on layer type
|
|
if ($layer_type === 'population') {
|
|
$value = $areaMetric['population_density'];
|
|
|
|
} else if ($layer_type === 'road_density') {
|
|
if ($roadsExist) {
|
|
$value = $areaRoadCounts[$area_id] / $areaMetric['area_km2'];
|
|
} else {
|
|
$value = deterministicRoadDensity($areaMetric['population_density'], $area_id);
|
|
}
|
|
|
|
} else if ($layer_type === 'damaged_roads_density') {
|
|
if ($roadsExist) {
|
|
$value = $areaDamagedRoadCounts[$area_id] / $areaMetric['area_km2'];
|
|
} else {
|
|
$fallbackRoadDensity = deterministicRoadDensity($areaMetric['population_density'], $area_id);
|
|
$value = deterministicDamagedDensity($fallbackRoadDensity, $area_id);
|
|
}
|
|
}
|
|
|
|
$value = (float)$value;
|
|
$values[] = $value;
|
|
|
|
$data_items[] = [
|
|
'area_id' => $area_id,
|
|
'area_name' => $areaMetric['name'],
|
|
'value' => $value,
|
|
'geometry' => $geometry,
|
|
'area_km2' => round($areaMetric['area_km2'], 6),
|
|
'population' => $areaMetric['population'],
|
|
'population_density' => round($areaMetric['population_density'], 6),
|
|
'value_source' => $valueSource
|
|
];
|
|
}
|
|
|
|
// Calculate min and max values
|
|
$min_value = count($values) > 0 ? min($values) : 0;
|
|
$max_value = count($values) > 0 ? max($values) : 0;
|
|
|
|
// If all values are same, set reasonable range
|
|
if ($min_value === $max_value && $min_value > 0) {
|
|
$min_value = $min_value * 0.8;
|
|
$max_value = $max_value * 1.2;
|
|
} else if ($min_value === $max_value) {
|
|
$min_value = 0;
|
|
$max_value = 1;
|
|
}
|
|
|
|
// Return success response
|
|
http_response_code(200);
|
|
echo json_encode([
|
|
'success' => true,
|
|
'layer_type' => $layer_type,
|
|
'data' => $data_items,
|
|
'min_value' => floatval($min_value),
|
|
'max_value' => floatval($max_value),
|
|
'admin_level' => $admin_level,
|
|
'value_source' => $valueSource
|
|
]);
|
|
|
|
} catch (PDOException $e) {
|
|
http_response_code(500);
|
|
echo json_encode([
|
|
'success' => false,
|
|
'error' => 'Database error: ' . $e->getMessage()
|
|
]);
|
|
} catch (Exception $e) {
|
|
http_response_code(400);
|
|
echo json_encode([
|
|
'success' => false,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
}
|
|
?>
|