Files
d1041231029-sig-b/api/get_choropleth_data.php
T
2026-06-02 12:40:37 +07:00

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()
]);
}
?>