Skip to content

Geographic Queries

Laravel Nusa provides geographic data including coordinates, boundaries, and postal codes. This guide shows how to work with geographic information for mapping, location services, and spatial analysis.

Working with Coordinates

Basic Coordinate Access

php
use Creasi\Nusa\Models\Province;

$province = Province::find('33'); // Central Java

// Get center coordinates
$latitude = $province->latitude;   // -6.9934809206806
$longitude = $province->longitude; // 110.42024335421

echo "Central Java center: {$latitude}, {$longitude}";

Boundary Coordinates

php
use Creasi\Nusa\Models\{Province, Regency};

$province = Province::find('33');

// Get boundary coordinates (if available)
$boundaries = $province->coordinates;

if ($boundaries) {
    echo "Province has " . count($boundaries) . " boundary points";
    
    // Convert to GeoJSON format
    $geoJson = [
        'type' => 'Feature',
        'properties' => [
            'name' => $province->name,
            'code' => $province->code
        ],
        'geometry' => [
            'type' => 'Polygon',
            'coordinates' => [$boundaries]
        ]
    ];
    
    // Use with mapping libraries
    return response()->json($geoJson);
}

Postal Code Queries

Find by Postal Code

php
use Creasi\Nusa\Models\Village;

// Find villages by postal code
$villages = Village::where('postal_code', '51111')->get();

foreach ($villages as $village) {
    echo "{$village->name}, {$village->district->name}";
}

// Find all postal codes in a province
$province = Province::find('33');
$postalCodes = $province->postal_codes;

echo "Postal codes in {$province->name}: " . implode(', ', $postalCodes);

Postal Code Validation

php
function validatePostalCode(string $postalCode, string $villageCode): bool
{
    $village = Village::find($villageCode);
    
    return $village && $village->postal_code === $postalCode;
}

// Usage
$isValid = validatePostalCode('51111', '3375011002');

Distance Calculations

Haversine Distance Formula

php
class GeographicHelper
{
    /**
     * Calculate distance between two points using Haversine formula
     * 
     * @param float $lat1 Latitude of first point
     * @param float $lon1 Longitude of first point
     * @param float $lat2 Latitude of second point
     * @param float $lon2 Longitude of second point
     * @return float Distance in kilometers
     */
    public static function calculateDistance(float $lat1, float $lon1, float $lat2, float $lon2): float
    {
        $earthRadius = 6371; // Earth's radius in kilometers
        
        $dLat = deg2rad($lat2 - $lat1);
        $dLon = deg2rad($lon2 - $lon1);
        
        $a = sin($dLat / 2) * sin($dLat / 2) +
             cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
             sin($dLon / 2) * sin($dLon / 2);
             
        $c = 2 * atan2(sqrt($a), sqrt(1 - $a));
        
        return $earthRadius * $c;
    }
    
    /**
     * Find nearest administrative region to given coordinates
     */
    public static function findNearestProvince(float $lat, float $lon): ?Province
    {
        $provinces = Province::whereNotNull('latitude')
            ->whereNotNull('longitude')
            ->get();
            
        $nearest = null;
        $minDistance = PHP_FLOAT_MAX;
        
        foreach ($provinces as $province) {
            $distance = self::calculateDistance(
                $lat, $lon,
                $province->latitude, $province->longitude
            );
            
            if ($distance < $minDistance) {
                $minDistance = $distance;
                $nearest = $province;
            }
        }
        
        return $nearest;
    }
}

// Usage
$userLat = -6.200000;
$userLon = 106.816666;

$nearestProvince = GeographicHelper::findNearestProvince($userLat, $userLon);
echo "Nearest province: {$nearestProvince->name}";

// Calculate distance between two cities
$jakarta = Regency::search('jakarta pusat')->first();
$semarang = Regency::search('kota semarang')->first();

$distance = GeographicHelper::calculateDistance(
    $jakarta->latitude, $jakarta->longitude,
    $semarang->latitude, $semarang->longitude
);

echo "Distance from Jakarta to Semarang: " . round($distance, 2) . " km";

Mapping Integration

Leaflet.js Integration

js
// Initialize map
const map = L.map('map').setView([-2.5, 118], 5); // Center on Indonesia

L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    attribution: '© OpenStreetMap contributors'
}).addTo(map);

// Load and display provinces
async function loadProvinces() {
    try {
        const response = await fetch('/nusa/provinces');
        const data = await response.json();
        
        data.data.forEach(province => {
            if (province.latitude && province.longitude) {
                // Add province marker
                const marker = L.marker([province.latitude, province.longitude])
                    .addTo(map)
                    .bindPopup(`
                        <strong>${province.name}</strong><br>
                        Code: ${province.code}<br>
                        Coordinates: ${province.latitude}, ${province.longitude}
                    `);
                
                // Add province boundary if available
                if (province.coordinates && province.coordinates.length > 0) {
                    const polygon = L.polygon(province.coordinates, {
                        color: 'blue',
                        fillColor: 'lightblue',
                        fillOpacity: 0.3
                    }).addTo(map);
                    
                    polygon.bindPopup(`<strong>${province.name}</strong>`);
                }
            }
        });
    } catch (error) {
        console.error('Error loading provinces:', error);
    }
}

// Load regencies when province is clicked
async function loadRegencies(provinceCode) {
    try {
        const response = await fetch(`/nusa/provinces/${provinceCode}/regencies`);
        const data = await response.json();
        
        data.data.forEach(regency => {
            if (regency.latitude && regency.longitude) {
                L.circleMarker([regency.latitude, regency.longitude], {
                    radius: 5,
                    color: 'red',
                    fillColor: 'orange',
                    fillOpacity: 0.8
                }).addTo(map)
                .bindPopup(`
                    <strong>${regency.name}</strong><br>
                    Province: ${regency.province_code}<br>
                    Code: ${regency.code}
                `);
            }
        });
    } catch (error) {
        console.error('Error loading regencies:', error);
    }
}

// Initialize
loadProvinces();

Google Maps Integration

js
class IndonesiaMap {
    constructor(mapElementId) {
        this.map = new google.maps.Map(document.getElementById(mapElementId), {
            zoom: 5,
            center: { lat: -2.5, lng: 118 }, // Center on Indonesia
            mapTypeId: 'terrain'
        });
        
        this.infoWindow = new google.maps.InfoWindow();
        this.markers = [];
        this.polygons = [];
    }
    
    async loadProvinces() {
        try {
            const response = await fetch('/nusa/provinces');
            const data = await response.json();
            
            data.data.forEach(province => {
                this.addProvinceMarker(province);
                this.addProvinceBoundary(province);
            });
        } catch (error) {
            console.error('Error loading provinces:', error);
        }
    }
    
    addProvinceMarker(province) {
        if (!province.latitude || !province.longitude) return;
        
        const marker = new google.maps.Marker({
            position: { lat: province.latitude, lng: province.longitude },
            map: this.map,
            title: province.name,
            icon: {
                url: 'https://maps.google.com/mapfiles/ms/icons/blue-dot.png'
            }
        });
        
        marker.addListener('click', () => {
            this.infoWindow.setContent(`
                <div>
                    <h3>${province.name}</h3>
                    <p>Code: ${province.code}</p>
                    <p>Coordinates: ${province.latitude}, ${province.longitude}</p>
                    <button onclick="loadRegencies('${province.code}')">
                        Load Regencies
                    </button>
                </div>
            `);
            this.infoWindow.open(this.map, marker);
        });
        
        this.markers.push(marker);
    }
    
    addProvinceBoundary(province) {
        if (!province.coordinates || province.coordinates.length === 0) return;
        
        const coordinates = province.coordinates.map(coord => ({
            lat: coord[0],
            lng: coord[1]
        }));
        
        const polygon = new google.maps.Polygon({
            paths: coordinates,
            strokeColor: '#0000FF',
            strokeOpacity: 0.8,
            strokeWeight: 2,
            fillColor: '#0000FF',
            fillOpacity: 0.1
        });
        
        polygon.setMap(this.map);
        this.polygons.push(polygon);
    }
    
    clearMarkers() {
        this.markers.forEach(marker => marker.setMap(null));
        this.markers = [];
    }
    
    clearPolygons() {
        this.polygons.forEach(polygon => polygon.setMap(null));
        this.polygons = [];
    }
}

// Initialize map
const indonesiaMap = new IndonesiaMap('map');
indonesiaMap.loadProvinces();

Spatial Queries

Point-in-Polygon Checks

php
class SpatialHelper
{
    /**
     * Check if a point is inside a polygon using ray casting algorithm
     */
    public static function pointInPolygon(float $lat, float $lon, array $polygon): bool
    {
        $x = $lon;
        $y = $lat;
        $inside = false;
        
        $j = count($polygon) - 1;
        for ($i = 0; $i < count($polygon); $i++) {
            $xi = $polygon[$i][1]; // longitude
            $yi = $polygon[$i][0]; // latitude
            $xj = $polygon[$j][1];
            $yj = $polygon[$j][0];
            
            if ((($yi > $y) !== ($yj > $y)) && 
                ($x < ($xj - $xi) * ($y - $yi) / ($yj - $yi) + $xi)) {
                $inside = !$inside;
            }
            $j = $i;
        }
        
        return $inside;
    }
    
    /**
     * Find which province contains the given coordinates
     */
    public static function findProvinceByCoordinates(float $lat, float $lon): ?Province
    {
        $provinces = Province::whereNotNull('coordinates')->get();
        
        foreach ($provinces as $province) {
            if ($province->coordinates && 
                self::pointInPolygon($lat, $lon, $province->coordinates)) {
                return $province;
            }
        }
        
        return null;
    }
}

// Usage
$userLat = -6.200000;
$userLon = 106.816666;

$province = SpatialHelper::findProvinceByCoordinates($userLat, $userLon);
if ($province) {
    echo "You are in: {$province->name}";
} else {
    echo "Location not found in any province";
}

Bounding Box Queries

php
class BoundingBoxHelper
{
    /**
     * Calculate bounding box for a set of coordinates
     */
    public static function calculateBoundingBox(array $coordinates): array
    {
        $minLat = $maxLat = $coordinates[0][0];
        $minLon = $maxLon = $coordinates[0][1];
        
        foreach ($coordinates as $coord) {
            $lat = $coord[0];
            $lon = $coord[1];
            
            $minLat = min($minLat, $lat);
            $maxLat = max($maxLat, $lat);
            $minLon = min($minLon, $lon);
            $maxLon = max($maxLon, $lon);
        }
        
        return [
            'min_lat' => $minLat,
            'max_lat' => $maxLat,
            'min_lon' => $minLon,
            'max_lon' => $maxLon
        ];
    }
    
    /**
     * Find administrative regions within bounding box
     */
    public static function findRegionsInBoundingBox(
        float $minLat, float $maxLat, 
        float $minLon, float $maxLon
    ): array {
        $provinces = Province::whereBetween('latitude', [$minLat, $maxLat])
            ->whereBetween('longitude', [$minLon, $maxLon])
            ->get();
            
        $regencies = Regency::whereBetween('latitude', [$minLat, $maxLat])
            ->whereBetween('longitude', [$minLon, $maxLon])
            ->get();
            
        return [
            'provinces' => $provinces,
            'regencies' => $regencies
        ];
    }
}

// Usage - Find regions in Jakarta area
$jakartaBounds = BoundingBoxHelper::findRegionsInBoundingBox(
    -6.4, -6.0,  // Latitude range
    106.6, 107.0 // Longitude range
);

echo "Found {$jakartaBounds['provinces']->count()} provinces";
echo "Found {$jakartaBounds['regencies']->count()} regencies";

Location Services

Address Geocoding

php
class LocationService
{
    /**
     * Get coordinates for an address
     */
    public function geocodeAddress(array $addressComponents): ?array
    {
        $village = Village::find($addressComponents['village_code']);
        
        if ($village && $village->latitude && $village->longitude) {
            return [
                'latitude' => $village->latitude,
                'longitude' => $village->longitude,
                'accuracy' => 'village'
            ];
        }
        
        $district = District::find($addressComponents['district_code']);
        if ($district && $district->latitude && $district->longitude) {
            return [
                'latitude' => $district->latitude,
                'longitude' => $district->longitude,
                'accuracy' => 'district'
            ];
        }
        
        $regency = Regency::find($addressComponents['regency_code']);
        if ($regency && $regency->latitude && $regency->longitude) {
            return [
                'latitude' => $regency->latitude,
                'longitude' => $regency->longitude,
                'accuracy' => 'regency'
            ];
        }
        
        $province = Province::find($addressComponents['province_code']);
        if ($province && $province->latitude && $province->longitude) {
            return [
                'latitude' => $province->latitude,
                'longitude' => $province->longitude,
                'accuracy' => 'province'
            ];
        }
        
        return null;
    }
    
    /**
     * Reverse geocoding - find address from coordinates
     */
    public function reverseGeocode(float $lat, float $lon): ?array
    {
        // Find nearest village
        $nearestVillage = $this->findNearestVillage($lat, $lon);
        
        if ($nearestVillage) {
            return [
                'village' => $nearestVillage,
                'district' => $nearestVillage->district,
                'regency' => $nearestVillage->regency,
                'province' => $nearestVillage->province,
                'full_address' => $this->buildFullAddress($nearestVillage)
            ];
        }
        
        return null;
    }
    
    private function findNearestVillage(float $lat, float $lon): ?Village
    {
        // This is a simplified version - in production, you'd use spatial indexes
        $villages = Village::whereNotNull('latitude')
            ->whereNotNull('longitude')
            ->get();
            
        $nearest = null;
        $minDistance = PHP_FLOAT_MAX;
        
        foreach ($villages as $village) {
            $distance = GeographicHelper::calculateDistance(
                $lat, $lon,
                $village->latitude, $village->longitude
            );
            
            if ($distance < $minDistance) {
                $minDistance = $distance;
                $nearest = $village;
            }
        }
        
        return $nearest;
    }
    
    private function buildFullAddress(Village $village): string
    {
        return implode(', ', [
            $village->name,
            $village->district->name,
            $village->regency->name,
            $village->province->name,
            $village->postal_code
        ]);
    }
}

// Usage
$locationService = new LocationService();

// Geocode an address
$coordinates = $locationService->geocodeAddress([
    'village_code' => '3375011002',
    'district_code' => '337501',
    'regency_code' => '3375',
    'province_code' => '33'
]);

if ($coordinates) {
    echo "Address coordinates: {$coordinates['latitude']}, {$coordinates['longitude']}";
    echo "Accuracy: {$coordinates['accuracy']}";
}

// Reverse geocode coordinates
$address = $locationService->reverseGeocode(-6.8969497174987, 109.66208089654);
if ($address) {
    echo "Address: {$address['full_address']}";
}

These examples demonstrate how to work with geographic data in Laravel Nusa for mapping, location services, and spatial analysis. The geographic features enable building sophisticated location-based applications with accurate Indonesian administrative data.

Released under the MIT License.