March 17

Custom Map Modules Using the AerisWeather Javascript SDK

About a year ago we first released our new AerisWeather Javascript SDK. Since then, it’s been a popular toolkit for many developers and companies to use in jump-starting their weather-based web applications. With our version 1.3.0 release, we introduced support for map modules to allow you to quickly extend your interactive maps or map applications with custom datasets and layers. You can now take advantage of the immense flexibility and architecture we’ve already established with our SDK for your own data.

So, What are Map Modules?

Map modules are self-contained data layer components that can be added to and rendered on InteractiveMap and InteractiveMapApp instances. Each module provides all of the necessary setup and configuration, such as layer styling and data requests. Map modules provide a map data source configuration for its data that should be rendered to the map. A map data source can be raster tile- or vector-based and is required whether the module is added to an InteractiveMap or InteractiveMapApp instance. However, modules added to an InteractiveMapApp can provide additional functionality, such as layer controls, legends, info panel content views and more.

Map modules provide the following configurations:

Therefore, map modules make it quick and easy to expand the data offered by your map applications with little effort.

Using Built-in Map Modules

Our AerisWeather Javascript SDK provides a number of built-in map modules that you can add as needed. Built-in map modules aren’t loaded into your application unless you need them since they’re lazy-loaded on-demand.

Currently, we offer two map module groups, each consisting of a series of individual modules:

  • Tropical
    • Active and archive tropical cyclones
    • Tropical invests
    • Watch/warning breakpoints
    • Cyclone wind fields
  • Aviation
    • Airport flight rules
    • Airport station plots
    • SIGMETs

You can add one or more of these modules to your applications. Additionally, you can add an entire group to your map applications, which adds each module from that group (InteractiveMapApp only). Read more about our built-in and external map modules in our online documentation.

Creating a Custom Map Module

Suppose you have a custom data source specific to your application or business you need to display on your map. Or, you want to use our weather data API to create advanced, feature-rich weather-related map layers. Creating a custom map module is a good and easy solution for these use-cases.

We’ve written detailed documentation on the process you and your developers need to follow when building custom map modules and module groups. So instead of repeating much of that information, we’re going to walk through the creation of our Observations map module that we provide as an external module. You can review and download the code from our public Github repository.

The Problem

The goal of our observations module is to display a variety of vector data points as markers based on observation data from our weather data API. We want to be able to toggle between different observation properties, each providing its own set of marker colors and styling. We also want to be able to toggle marker data values between imperial and metric units. Finally, we want to display the built-in local weather info panel for a particular location when its marker is selected on the map.

Getting Started

Now with our goal established, we can start building. Depending on your use-case and the complexity of your custom map module, your modules may be developed as separate files within your project, a separate NPM project that can be imported and shared, or using the pre-compiled method. We’ve already outlined these methods in our online SDK docs. We decided to build our Observations module as a separate NPM project since we wanted it to be separate from the core SDK codebase and so we can share it with you and our users.

Map Module Starter Project

To make getting started even easier, we’ve set up a map module starter project that you can download on Github. We will be using this starter project for our Observations module. Once you’ve downloaded the starter project, unzip the downloaded archive to your desired destination. Then use a command-line terminal to finish setting up your project:

cd aeris-module-starter
sh setup.sh

The setup script will prompt you for information to configure your NPM package and core source files based on your module’s desired configuration. However, you can always change this information later once your project has been set up initially. We’ve set our observations NPM package name to

awxjs-observations-module

  and the module class name

Observations

 .

Now that our project is set up, we’ll start building out the module’s functionality in the project’s

src/Observations.ts

  file that was created for us when setting up the project. You’ll notice the starter project is set up using Typescript, but you can use native Javascript instead by renaming your file extensions from

.ts

  to

.js

  and removing Typescript syntax. Babel is then used with the starter project to compile Typescript to ES6 Javascript, and Webpack is used to bundle your module to a single script file that can easily be included on any HTML page.

Setting Up a Data Source

The core code and functionality of our module can now be added to the project’s

src/Observations.ts

  file. The starter project generated our module file for us which looks like the following:

import MapSourceModule from '@aerisweather/javascript-sdk/dist/modules/MapSourceModule';

class Observations extends MapSourceModule {

    public get id() {
        return 'observations';
    }

    constructor(opts: any = null) {
        super(opts);

        // Perform additional custom setup required by the module.
    }

    source(): any {
        // Setup and return the map content source that's used to load and render your module's
        // data on the map
        // re: https://www.aerisweather.com/docs/js/classes/tilesource.html
        // re: https://www.aerisweather.com/docs/js/classes/vectorsource.html

        return null;
    }

    controls(): any {
        // Setup and return the layer button configuration for this module. If 'null' is returnd,
        // then this module will not include a toggleable control within the map application.
        // re: https://www.aerisweather.com/docs/js/globals.html#buttonoptions

        return {
            value: this.id,
            title: 'Observations'
        };
    }

    legend(): any {
        // Create and return the legend configuration for this module. If 'null' is returned, then
        // a legend will not be rendered when this module's map source is active.
        // re: https://www.aerisweather.com/docs/js/globals.html#legendoptions

        return null;
    }

    infopanel(): any {
        // Create and return the info panel configuration to associate with data loaded and
        // rendered by this module's map source. If a custom info panel view is not needed for this
        // module, just return 'null'.
        // re: https://www.aerisweather.com/support/docs/toolkits/aeris-js-sdk/interactive-map-app/info-panel/
        // re: https://www.aerisweather.com/docs/js/globals.html#infopanelviewsection

        return null;
    }

    onInit() {
        // Perform custom actions when the module has been initialized with a map application
        // instance.
    }

    onAdd() {
        // Perform custom actions when the module's map source has been added to the map and is
        // active.
    }

    onRemove() {
        // Perform custom actions when the module's map source has been removed from the map and
        // is no longer active.
    }

    onMarkerClick(marker: any, data: any) {
        // Perform custom actions when a marker object associated with the module's map source was
        // clicked on the map. You can use this method to perform additional actions or to display
        // the info panel for the module with the marker's data, e.g.:
        //
        // this.showInfoPanel('Observation', data);
    }

    onShapeClick(shape: any, data: any) {
        // Perform custom actions when a vector shape object associated with the module's map
        // source was clicked on the map. You can use this method to perform additional actions or
        // to display the info panel for the module with the shape's data, e.g.:
        //
        // this.showInfoPanel('Outlook', data);
    }
}

export default Observations;

At a minimum, our module must define the data source configuration to represent the module’s data on the map. We’ve provided an overview of the supported map data sources and their configurations when developing your own modules. We will be configuring our module with a vector data source since we want to use data from the AerisWeather Data API.

Configuring the Vector Data Source

A vector data source consists of two primary configurations: the data and the style. Since we’re using observation data from the AerisWeather Data API, we can just use an instance of ApiRequest for performing the necessary API requests for our module’s data and assign it to the service property, which can either be an instance of ApiRequest or a function that returns an instance of ApiRequest:

source(): any {
    return {
        type: 'vector',
        refresh: 300,
        requiresBounds: true,
        data: {
            service: () => {
                const request = this.account.api()
                    .endpoint('observations')
                    .action(ApiAction.WITHIN)
                    .filter('allstations,allownosky')
                    .sort('id:1')
                    .limit(1000);
                return request;
            },
            properties: {
                timestamp: 'periods.ob.timestamp'
            }
        }
    }
}

We also set the source’s

data.properties.timestamp

property to inform the data source about the property key path to use for each result’s date and time information. Our data plotted on the map would be similar to the following:

Adding a vector data source to a map

Using Evenly-Distributed Markers

However, we really want our observations to be spaced apart across the visible map region rather than clustered in certain areas. We can take advantage of our API’s new lod (level-of-detail) parameter to do this. Our ApiRequest instance just needs to be set on our module and set up when the module is initialized using

onInit() to

set the level-of-detail based on the map’s current zoom level. Then we just return a reference to that request instance in our data source’s data configuration. Our module file now looks like the following:

import MapSourceModule from '@aerisweather/javascript-sdk/dist/modules/MapSourceModule';
import ApiRequest, { ApiAction } from '@aerisweather/javascript-sdk/dist/network/api/ApiRequest';

export type ObservationsOpts = {};

class Observations extends MapSourceModule {
    private _request: ApiRequest;

    public get id() {
        return 'observations';
    }

    constructor(opts: ObservationsOpts = null) {
        super(opts);
    }

    source(): any {
        return {
            type: 'vector',
            refresh: 300,
            requiresBounds: true,
            data: {
                service: () => {
                    return this._request;
                },
                properties: {
                    timestamp: 'periods.ob.timestamp'
                 }
            }
        };
    }

    onInit() {
        const request = this.account.api()
            .endpoint('observations')
            .action(ApiAction.WITHIN)
            .lod(this.map.getZoom())
            .filter('allstations,allownosky')
            .sort('id:1')
            .limit(1000);
        this._request = request;

        this.map.on('zoom', () => {
            this._request.lod(this.map.getZoom());
        });
    }
}

Testing our module data, we should now see something like the following:

Configure map module data to be evenly-distributed

That’s it for setting up our module’s data source. Next, we’ll look at styling our map objects.

Styling Map Markers

For our data’s styling, we can take advantage of the SDK’s flexible customization options. A series of markers will represent our observation data on the map. We want to style markers based on the type of data being viewed and the value of each point. Thus, we can use an SVG-based marker configuration and return it as a function since it will change depending on data type and value. We’ve set up a series of color ramp objects by observation property and utility functions within our module that we’ll reference in order to return the correct color value for each marker.

To control the styling based on data type, we’ll need to keep track of the selected observation property within our module instance. We can do this using a private property on our module’s class,

_weatherProp

 , which we will assign to

temps 

and update based on user selection later. Our marker style configurator function can reference this property when determining the appropriate marker color:

source(): any {
    return {
        type: 'vector',
        refresh: 300,
        requiresBounds: true,
        data: {
            service: () => {
                return this._request;
            },
            properties: {
                timestamp: 'periods.ob.timestamp'
             }
        },
        style: {
            marker: (data: any) => {
                // observation data for the marker instance
                const ob = data.ob;

                // the active observation property type being viewed
                const type = this._weatherProp;

                // whether values should be displayed in Metric
                const isMetric = false;

                // get the marker's value based on the active property type
                const value = get(ob, getObsProp(type, false));

                // format the marker's text label based on the property type
                const valueLabel = formatMeasurement(value, type, isMetric);

                // get the marker color for the marker's value and property type
                const valueColor = colorForValue(getColorRamp(type), value) || 'rgba(0,0,0,0)';

                // only render marker if we have a valid numerical value
                if (shouldAllowMarker(type, value)) {
                    return {
                        svg: {
                            shape: {
                                type: 'circle',
                                fill: {
                                    color: valueColor
                                },
                                stroke: {
                                    color: '#ffffff',
                                    width: 2
                                }
                            },
                            text: {
                                value: 
${valueLabel}

,
anchor: 'start',
position: 'center',
color: isLight(valueColor) ? '#222222' : '#ffffff',
autosize: false,
translate: {
x: -0.5,
y: -2
}
}
},
size: [30, 30]
};
}

// marker's value is invalid, so skip this marker so it doesn't get rendered on the map
return {
skip: true
}
}
}
};
}
Our module’s markers should now look substantially better with our new styling in place:

Styling map module marker elements

With our custom styling configured and working properly, let’s move onto configuring our layer’s control component.

Configuring a Layer Control

Configuring a layer control for your module provides you (and other users of your module) a method of controlling the visibility, filters and other data options associated with your module when added to an InteractiveMapApp instance. These controls are then added to the map application’s layers panel based on your configuration.

For our observations module, we want the user to be able to toggle between different observation properties, such as temperatures, dew points, wind speeds, etc. We also want to support toggling marker data between Imperial and Metric units. Therefore, we can configure our control as a segmented button that acts as a filter for our observations layer.

Using Button Segment Groups

Starting with version 1.3.0 of the SDK, button segments can also be grouped. Segment groups work well if you want to support multiple filters for a single layer, each group mapping to a specific parameter or property of the request. You define a segment group by providing an array of group configurations on your button’s segment property. In our case, we want a “Property” and “Units” group:

controls(): any {
    return {
        value: this.id,
        title: 'Observations',
        filter: true,
        segments: {
            groups: [{
                id: 'property',
                title: 'Property',
                segments: [{
                    value: 'temps',
                    title: 'Temperatures'
                },{
                    value: 'feelslike',
                    title: 'Feels Like'
                },{
                    value: 'winds',
                    title: 'Winds'
                },{
                    value: 'dewpt',
                    title: 'Dew Point'
                },{
                    value: 'humidity',
                    title: 'Humidity'
                },{
                    value: 'precip',
                    title: 'Precipitation'
                },{
                    value: 'sky',
                    title: 'Sky Cover'
                }]
            },{
                id: 'units',
                title: 'Units',
                segments: [{
                    value: 'imperial',
                    title: 'Imperial'
                },{
                    value: 'metric',
                    title: 'Metric'
                }]
            }]
        }
    };
}

Our observations module now appears like this when added to an InteractiveMapApp instance:

Configuring a map module control element

Updating Data on Layer Option Changes

Now, a single value can be selected within each group and we’ll be notified when the selected value from either group changes. You can respond to these changes by setting up an observer on the InteractiveMapApp’s

layer:change:option

  event. In our observation module, we need to set up an event handler for this event and update the active weather property type and units based on the control’s new values as demonstrated below:

this.app.on('layer:change:option', (e: any) => {
    // segmented button groups provide a 
value

as an object, where the selected value from each group
// is keyed the group's configured
const { id, value: { property, units } } = e.data || {};

if (id === this.id) {
this._weatherProp = property;
this._units = units;

// limit the fields returned based on the active weather property
this._request.fields(

id,loc,ob.dateTimeISO,ob.timestamp,ob.${getObsProp(property)},ob.${getObsProp(property, true)}

);
}
});
In the above code,

this._units

  just refers to a private property on our Observations class. Our final step is to update the data source’s marker style configurator to display the proper observation value based on the selected unit value assigned to

_units

 :

source(): any {
    return {
        data: {
            ...
        },
        style: {
            marker: (data: any) => {
                // observation data for the marker instance
                const ob = data.ob;

                // the active observation property type being viewed
                const type = this._weatherProp;

                // whether values should be displayed in Metric
                const isMetric = this._units === 'metric';

                ...
            }
        }
    }
}

That’s it for our layer’s control configuration. We can now toggle between various data properties and units at runtime on the map:

Toggling between layer options

Final Steps

Map modules support several other options, such as:

  • configuring a legend to display with your module’s map data
  • displaying additional information in a custom info panel when markers and/or polygons are selected on the map
  • performing additional actions when the module’s layer is added and/or removed from the map
  • responding to marker/polygon selections on the map

When one of our observation markers is selected, the map application’s info panel should display the built-in local weather information. For this, we’ll just need to add our code to the module’s

onMarkerClick

  method:

onMarkerClick(marker: any, data: any) {
    const { lat, long: lon } = data.loc || { lat: null, lon: null };
    if (isset(lat) && isset(lon)) {
        this.app.showInfoAtCoord({ lat, lon }, 'localweather', 'Local Weather');
    }
}

Now when clicking on one of the markers from our module’s layer, the local weather info panel will appear with detailed weather information for that location:

Showing local weather information within the info panel

And that’s it, we’ve set up our Observations module and it’s now ready to use!

Using Our Map Module

Both InteractiveMap and InteractiveMapApp instances support map modules. Since only InteractiveMapApp instances use layer controls, legends, and info panels, InteractiveMap instances only add the data source from the module.

Map Modules with InteractiveMap

You’ll need to import your module into the file where you’ll be implementing it with InteractiveMap. Then just use the

addModule()

  method on your map instance to add an instance of the module and show the layer on the map immediately:

import InteractiveMap from '@aerisweather/javascript-sdk/dist/maps/interactive/InteractiveMap';
import Observations from '@aerisweather/awxjs-observations-module/dist/Observations';

const map = new InteractiveMap(document.getElementById('#map'), {...});
map.addModule(new Observations());

To remove the layer from your map afterward, get the source based on the module’s identifier and use

removeSource()

  on your map to remove it:

const source = map.getSourceForId('observations');
if (source) {
    map.removeSource(source);
};

Map Modules with InteractiveMapApp

Adding a map module to an InteractiveMapApp instance is similar to that of InteractiveMap. However, you’ll add the module to the map application’s module manager instead of the interactive map directly:

import InteractiveMapApp from '@aerisweather/javascript-sdk/dist/apps/InteractiveMapApp';
import Observations from '@aerisweather/awxjs-observations-module/dist/Observations';

const app = new InteractiveMapApp(document.getElementById('#app'), {...});
app.modules.add(new Observations());

You don’t need to remove modules directly from your application since their layers are toggled on/off the map using the layer control panel. If your map module does not have layer controls configured, then you can use

removeSource()

  on the application’s interactive map directly as described above.

Share Your Map Modules

Once you’ve created your own map modules, reach out to us if you’d like to share your map modules for others to use in their own weather map applications. If you’re not currently taking advantage of the power and rich feature set offered by our Javascript SDK, skim through the online documentation and examples for inspiration.

Not an AerisWeather customer yet? Sign up for a free trial of our Weather Data API and Aeris Mapping Platform (AMP).

 

Share this post:

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.

AerisWeather
{{page_content}}