Navigraph Docs Logo

Airport Map Tutorial

An example for how to create an interactive airport map using Maplibre and React

In this tutorial, you will learn how to query the AMDB using the JS SDK, and render it using the Maplibre mapping package.

This tutorial builds upon the SDK Quickstart, so please complete that tutorial first (or use the linked CodeSandbox), and then begin this tutorial in the same project.

1. Install Mapping Dependencies

This tutorial will use the Maplibre GL JS engine (which uses webgl) for map rendering, and the react-map-gl library for composition into the react structure.

npm i react-map-gl maplibre-gl

2. Add AMDB Scope

In order to make use of the AMDB API, the application must request access to the AMDB scope, and export the AMDB interface from the file.

Specifying a scope in the application does not neccessarily mean that your client credentials have permission to request said scope. If you request a scope that your client does not have permission for, the authentication flow will enter an error state.

src/lib/navigraph.ts
// ...

import { getAmdbAPI } from 'navigraph/amdb'

const config: NavigraphApp = {
  clientId: "<YOUR_NAVIGRAPH_CLIENT_ID>",
  clientSecret: "<YOUR_NAVIGRAPH_CLIENT_SECRET>",
  scopes: [Scope.CHARTS], 
  scopes: [Scope.CHARTS, Scope.AMDB], 
};

// ...

export const charts = getChartsAPI();
export const amdb = getAmdbAPI(); 

3. Create the Map Renderer

Now we will create our map pane component. Create a new file, with a default exported component which, for now, has no props.

src/components/map-renderer.ts
import { Layer, Map, Source } from "react-map-gl/maplibre";
import "maplibre-gl/dist/maplibre-gl.css"; // Import maplibre CSS so the map containers work properly

export default function MapRenderer() {
  return (
    <Map style={{ height: "500px" }}>
      {/* Render a basemap of open street map tiles */}
      <Source type="raster" tiles={["https://tile.openstreetmap.org/{z}/{x}/{y}.png"]}>
        <Layer id="satellite-basemap" type="raster" />
      </Source>
    </Map>
  );
}

Then, in your App.tsx file, import the Map Renderer, and render it when the user is logged in, replacing the previously displayed username information. This will be refactored again, but just check that you get the expected results.

src/App.tsx
// ...

import MapRenderer from "./components/map-renderer"; 

// ...

    {params?.verification_uri_complete && !user && (
        <>
            <QRCodeSVG value={params.verification_uri_complete} size={250} />
            <a href={params.verification_uri_complete} target="_blank" rel="noreferrer">
                Open sign in page
            </a>
        </>
    )}

    {user && ( 
        <h2> 
            Welcome, <strong>{user.preferred_username}</strong>
        </h2>
    )}
    {user && <MapRenderer />} 
</main>

// ...

You should now see a basic basemap rendering when the user is logged in, and the map should be interactive.

Screenshot 1

Data Loading

Now that we have a working interactive map renderer, we need to query and render AMDB data. To do this, we will create another component which wraps the map renderer, which will handle fetching of AMDB data.

For the react data fetching, we will use the swr library. Install it using the following command

npm i swr

Since the wrapper will handle fetching the data, we need to change the map renderer to accept the data and render it

src/components/map-renderer.tsx
import { Layer, Map, Source } from "react-map-gl/maplibre";
import "maplibre-gl/dist/maplibre-gl.css"; // Import maplibre CSS so the map containers work properly
import type { AmdbLayerName, AmdbResponse } from "navigraph/amdb"; 

// List of layers to load and render
export const LAYERS = ["aerodromereferencepoint", "taxiwayelement", "runwayelement"] satisfies AmdbLayerName[];

interface MapRendererProps {
  data: AmdbResponse<(typeof LAYERS)[number]>;
}

export default function MapRenderer() { 
export default function MapRenderer({ data }: MapRendererProps) {  
  return (
    <Map 
      style={{ height: "500px" }}
      // Initially focus on the aerodrome reference point of the loaded airport
      initialViewState={{
        zoom: 12,
        longitude: data.aerodromereferencepoint.features[0].geometry.coordinates[0],
        latitude: data.aerodromereferencepoint.features[0].geometry.coordinates[1],
      }}
    >
      {/* Render a basemap of open street map tiles */}
      <Source type="raster" tiles={["https://tile.openstreetmap.org/{z}/{x}/{y}.png"]}>
        <Layer id="satellite-basemap" type="raster" />
      </Source>

      {/* New AMDB Layers */}
      <Source type="geojson" data={data.runwayelement}>
        <Layer
          id="runwayelement"
          type="fill"
          paint={{
            "fill-color": "gray",
          }}
        />
      </Source>

      <Source type="geojson" data={data.taxiwayelement}>
        <Layer
          id="taxiwayelement"
          type="fill"
          paint={{
            "fill-color": "gray",
          }}
        />
      </Source>
    </Map>
  );
}

Now create the wrapper component in a new file

src/components/map-pane.tsx
import useSWR from "swr";
import { amdb } from "../lib/navigraph";
import MapRenderer, { LAYERS } from "./map-renderer";

// Stockholm Arlanda Airport
const AIRPORT = "ESSA";

export default function MapPane() {
  const { data } = useSWR(["amdb-layers", AIRPORT, LAYERS], () => {
    return amdb.getAmdbLayers({
      icao: AIRPORT,
      include: LAYERS,
    });
  });

  if (!data) {
    return <span>Loading...</span>;
  }

  return <MapRenderer data={data} />;
}

And update App.tsx to use the wrapper component

import MapRenderer from "./components/map-renderer"; 
import MapPane from "./components/map-pane"; 

// ..

  {user && <MapRenderer />} 
  {user && <MapPane />} 

You should now see a zoomed in map of Arlanda with taxiways and runways rendered ontop in gray

Screenshot 2

To add more layers, simply add to the LAYERS list, and add new sources and layers tags in MapRenderer

On this page