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