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.
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.
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:
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.
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 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.
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.
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.
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.
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:
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:
That’s it for setting up our module’s data source. Next, we’ll look at styling our map objects.
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: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:
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 avalueas 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:
Final Steps
Map modules support several other options, such as:
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:
And that’s it, we’ve set up our Observations module and it’s now ready to use!
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.
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); };
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.
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).
No comments yet.
Be the first to respond to this article.