Skip to content
AUTK-DBAUTK-MAP

Spatial Join in the Browser

Combine spatial datasets in the browser and visualize the results instantly. This example loads neighborhood polygons and point data, performs a spatial join with autk-db, and renders the aggregated result with autk-map.

Live Example

Objective

Load two spatial layers, perform a spatial join, and theme the result on the map. This example highlights a key transition in Autark: it is not just rendering, but real in-browser spatial analysis, without PostGIS, GIS servers, or backend infrastructure.

Source Code

import { SpatialDb } from 'autk-db';
import { AutkMap, LayerType } from 'autk-map';
import { Feature, FeatureCollection, GeoJsonProperties } from 'geojson';

type CountRow = {
    neighborhood_id: string;
    noise_count: number;
};

async function main() {
    const canvas = document.querySelector('canvas')!;
    const statusEl = document.getElementById('loading-status');
    const loadingText = document.getElementById('loading-text');

    const setStatus = (msg: string) => {
        if (loadingText) loadingText.textContent = msg;
        if (statusEl) statusEl.style.display = 'flex';
    };

    const hideStatus = () => {
        if (statusEl) statusEl.style.display = 'none';
    };

    try {
        const baseUrl = window.location.origin;
        const neighborhoodsUrl = `${baseUrl}/data/mnt_neighs.geojson`;
        const noiseUrl = `${baseUrl}/data/noise.geojson`;

        setStatus('Initializing spatial database...');
        const db = new SpatialDb();
        await db.init();

        setStatus('Loading neighborhood polygons...');
        await db.loadCustomLayer({
            geojsonFileUrl: neighborhoodsUrl,
            outputTableName: 'neighborhoods',
            coordinateFormat: 'EPSG:3395',
        });

        setStatus('Loading point dataset...');
        await db.loadCustomLayer({
            geojsonFileUrl: noiseUrl,
            outputTableName: 'noise',
            coordinateFormat: 'EPSG:3395',
        });

        setStatus('Running spatial join in the browser...');
        const counts = await db.rawQuery({
            query: `
                SELECT
                    struct_extract(neighborhoods.properties, 'nta2020') AS neighborhood_id,
                    COUNT(noise.geometry) AS noise_count
                FROM neighborhoods
                LEFT JOIN noise
                    ON ST_Intersects(neighborhoods.geometry, noise.geometry)
                GROUP BY 1
            `,
            output: {
                type: 'RETURN_OBJECT',
            },
        }) as CountRow[];

        const countsByNeighborhood = new Map<string, number>();
        for (const row of counts) {
            countsByNeighborhood.set(
                String(row.neighborhood_id),
                Number(row.noise_count || 0)
            );
        }

        setStatus('Preparing joined GeoJSON...');
        const neighborhoodsGeojson = await db.getLayer('neighborhoods');
        const noiseGeojson = await db.getLayer('noise');

        const themedNeighborhoods: FeatureCollection = {
            ...neighborhoodsGeojson,
            features: neighborhoodsGeojson.features.map((feature: Feature) => {
                const properties = (feature.properties ?? {}) as Record<string, any>;
                const neighborhoodId = String(properties.nta2020 || '');
                const noiseCount = countsByNeighborhood.get(neighborhoodId) || 0;

                return {
                    ...feature,
                    properties: {
                        ...properties,
                        sjoin: {
                            count: {
                                noise: noiseCount,
                            },
                        },
                    },
                };
            }),
        };

        setStatus('Rendering joined layers...');
        const map = new AutkMap(canvas);
        await map.init();

        map.loadGeoJsonLayer(
            'neighborhoods',
            themedNeighborhoods,
            LayerType.AUTK_GEO_POLYGONS
        );

        map.loadGeoJsonLayer(
            'noise',
            noiseGeojson,
            LayerType.AUTK_GEO_POINTS
        );

        const getJoinCount = (feature: Feature) => {
            const properties = feature.properties as GeoJsonProperties;
            return properties?.sjoin?.count?.noise || 0;
        };

        map.updateGeoJsonLayerThematic('neighborhoods', themedNeighborhoods, getJoinCount);

        map.draw();
        hideStatus();
    } catch (err) {
        const msg = err instanceof Error ? err.message : 'An unexpected error occurred';
        if (loadingText) loadingText.textContent = `Error: ${msg}`;
        if (statusEl) {
            statusEl.style.background = 'rgba(254,242,242,0.95)';
            const spinner = statusEl.querySelector('.autk-spinner') as HTMLElement | null;
            if (spinner) spinner.style.display = 'none';
        }
        console.error(err);
    }
}

main();

Released under the MIT License.