Introduction
If you are not using Mapbox or need complete customization, you can access the un-styled MapBox Vector tiles using the AirMap Tile Server API. However, we only recommend this for advanced users. If you just want to display AirMap airspace on a web page or mobile, check out our Maps Overview guide. The following example explains how to interact with the tile server in Javascript, but the same flow would apply to any other language.
Jurisdictions
Before we can get the actual airspace tiles, we need to know which airspace to retrieve in a given area since each area has different rules and airspace. To figure this out dynamically, there is a hidden layer jurisdictions
that provides all of the different rulesets for a given tile. This includes national and local rules that are available. Let's start by adding this jurisdictions
layer to the map:
Private Jurisdictions and Rulesets (optional)
In order to access private jurisdictions and rulesets that only the user has access to, you will have to pass the logged in user's access token as a parameter in the tile url.
// On map load
map.on('load', () => {
// Add Jurisdictions
map.addLayer({
"id": "jurisdictions",
"type": "fill",
"source": {
type: 'vector',
tiles: [ 'https://api.airmap.com/tiledata/v1/base-jurisdiction/{z}/{x}/{y}?access_token=$AIRMAP_ACCESS_TOKEN' ],
"minzoom": 6,
"maxzoom": 12
},
"source-layer": "jurisdictions",
"minZoom": 6,
"maxZoom": 22
}, 'background')
}
Once we add this layer to the map, we need to parse the jurisdictions to get each ruleset for a tile. We can parse it (removing duplicate or empty jurisdictions) using a function like so:
function parseJurisdictions() {
var layers = map.queryRenderedFeatures()
//iterate through layers
var jurisdictions = layers.filter(x => x.layer.source == "jurisdictions" && x.properties.jurisdiction)
.map(feature => JSON.parse(feature.properties.jurisdiction))
// remove duplicate or empty jurisdictions
jurisdictions = jurisdictions.filter(x => x.rulesets.length > 0).filter((obj, pos, arr) => {
return arr.map(mapObj => mapObj["uuid"]).indexOf(obj["uuid"]) === pos;
});
return jurisdictions;
}
Note that we should only parse the jurisdiction layer after it is loaded. We can check for that like this:
map.on('sourcedata', (data) => {
// check for jurisdiction
if(data.sourceId == 'jurisdictions' && data.isSourceLoaded) {
// parse jurisdictions here...
}
})
Rulesets
Now we will need to get the rulesets from our jurisdictions. Each jurisdiction will include features as rulesets, which we can obtain by using a function like so:
function addRulesets(jurisdictions) {
let rulesets = [];
jurisdictions.forEach(jurisdiction => {
jurisdiction.rulesets.forEach(ruleset => {
if (ruleset.selection_type == "pick1") {
if (ruleset.id == "usa_part_107")
rulesets.push(ruleset);
} else {
rulesets.push(ruleset);
}
})
})
return rulesets;
}
This will return all available rulesets present for the jurisdictions in a particular area. The rulesets should then be filtered based on their selection type: "required", "pick1", or "optional". In this example, we select US Part 107 rules as our pick1 option and we will be adding all optional rulesets as well.
Loading Layers
Now we will need to load the actual airspace layers from the jurisdictions. For each ruleset, we add a tile source (using the tile server API) and for each layer in that ruleset, we add a layer to our map.
rulesets.forEach(ruleset => {
// Add vector tiles source for each ruleset
map.addSource(ruleset.id, {
type: 'vector',
tiles: [ 'https://api.airmap.com/tiledata/v1/'+ruleset.id+'/'+ruleset.layers.join(',')+'/{z}/{x}/{y}?apikey=$AIRMAP_API_KEY&access_token=$AIRMAP_ACCESS_TOKEN' ],
"minzoom": 6,
"maxzoom": 12
})
ruleset.layers.forEach(layer => {
// All layers for the ruleset
let airspaceLayer = {
"id": ruleset.id+"_"+layer,
"source": ruleset.id,
"source-layer": ruleset.id+"_"+layer,
"type": "fill",
"interactive": true,
"paint": {
"fill-opacity": 0.2,
"fill-color": #1fa0d3
}
}
map.addLayer(airspaceLayer, ruleset.id)
})
})
Styling
Style within a single layer
For some layers, you may want to style tiles differently based on an attribute. In this case, you can modify the style properties of each layer before adding to the map. The tile server API will return different airspace information for each layer, so you can customize each layer based on its own properties.
Styles from AirMap CDN
You can also fetch map styles from AirMap's CDN for a specific theme and version. The returned JSON will have custom styles for layers which we can then add to our map. For example, we can use the following link to fetch map styles for the standard theme:
https://cdn.airmap.com/static/map-styles/0.8.5/standard.json
Example
The following code block shows an example in HTML/JavaScript/CSS of how to use the AirMap Tile Server API to retrieve tile data and add layers to a MapBox application.
<!DOCTYPE html>
<html lang="en">
<head>
<title>AirMap | Maps SDK</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<script src="https://cdn.airmap.io/js/contextual-airspace/1.0.0/airmap.contextual-airspace-plugin.min.js" async=false defer=false></script>
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v0.39.1/mapbox-gl.js"></script>
<link href="https://api.tiles.mapbox.com/mapbox-gl-js/v0.39.1/mapbox-gl.css" rel="stylesheet" />
<style>
body {
margin:0;
padding:0;
}
#map {
position: absolute;
width: 100%;
height: 100%;
z-index: -1
}
</style>
</head>
<body>
<div id="map"></div>
<script>
// Include your airmap api key
var AIRMAP_API_KEY = "{API KEY}"
// Include your mapbox access token
var MAPBOX_ACCESS_TOKEN = "{ACCESS TOKEN}"
if (AIRMAP_API_KEY && MAPBOX_ACCESS_TOKEN) {
mapboxgl.accessToken = MAPBOX_ACCESS_TOKEN
// Create an instance of the map
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/airmap/cipg7gw9u000jbam53kwpal1q',
center: [-118.496475, 34.024212],
zoom: 10
})
function getRandomColor() {
var letters = '0123456789ABCDEF';
var color = '#';
for (var i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
function addRulesets(jurisdictions){
let rulesets = [];
jurisdictions.forEach(jurisdiction => {
jurisdiction.rulesets.forEach(ruleset => {
if (ruleset.selection_type == "pick1") {
if (ruleset.id == "usa_part_107")
rulesets.push(ruleset);
} else {
rulesets.push(ruleset);
}
})
})
return rulesets;
}
function parseJurisdictions(){
var layers = map.queryRenderedFeatures()
// Iterate through layers
var jurisdictions = layers.filter(x => x.layer.source == "jurisdictions" && x.properties.jurisdiction)
.map(feature => JSON.parse(feature.properties.jurisdiction))
// Remove duplicate or empty jurisdictions
jurisdictions = jurisdictions.filter(x => x.rulesets.length > 0).filter((obj, pos, arr) => {
return arr.map(mapObj => mapObj["uuid"]).indexOf(obj["uuid"]) === pos;
});
return jurisdictions;
}
// Add layers
map.on('load', () => {
// Add Jurisdictions
map.addLayer({
"id": "jurisdictions",
"type": "fill",
"source": {
type: 'vector',
tiles: [ 'https://api.airmap.com/tiledata/v1/base-jurisdiction/{z}/{x}/{y}' ],
"minzoom": 6,
"maxzoom": 12
},
"source-layer": "jurisdictions",
"minZoom": 6,
"maxZoom": 22
}, 'background')
})
map.on('sourcedata', (data) =>{
// Check for jurisdiction
if(data.sourceId == 'jurisdictions' && data.isSourceLoaded){
// Parse jurisdiction
var jurisdictions = parseJurisdictions();
// Add rulesets
var rulesets = addRulesets(jurisdictions);
rulesets.forEach(ruleset => {
// Add Part 107 Source
map.addSource(ruleset.id, {
type: 'vector',
tiles: [ 'https://api.airmap.com/tiledata/v1/'+ruleset.id+'/'+ruleset.layers.join(',')+'/{z}/{x}/{y}?apiKey=$AIRMAP_API_KEY' ],
"minzoom": 6,
"maxzoom": 12
})
ruleset.layers.forEach(layer => {
// All Controlled Airspace
let airspaceLayer = {
"id": ruleset.id+"_"+layer,
"source": ruleset.id,
"source-layer": ruleset.id+"_"+layer,
"type": "fill",
"interactive": true,
"paint": {
"fill-opacity": 0.4,
"fill-color": getRandomColor()
}
}
map.addLayer(airspaceLayer, ruleset.id)
})
})
}
})
} else {
console.error(
'Missing AIRMAP_API_KEY or MAPBOX_ACCESS_TOKEN. ' +
'These are required for developing the Maps SDK locally.\n\n'
);
}
</script>
</body>
</html>
Updated 3 years ago