Building a custom

AMD module

Use Case:

Real-time earthquake heatmap

Custom Classes

Stability

Scalability

Re-usability

Productivity!

https://pixabay.com/en/recycle-bin-container-recycling-24543/

Image CC0 Public Domain https://pixabay.com/en/spaghetti-bacon-pasta-894077/

require(["esri/map", "dojo/domReady!"], function(Map) {
  map = new Map("map", {
    basemap: "topo",  
    center: [-122.45, 37.75], 
    zoom: 13
  });
});

AMD modules...woohoo!

AMD Modules 

Written in JavaScript

Logical subsets of functionality

Protect internal content

Portable/Reusable

 

TO-DO List

Retrieve the GeoJSON data

Convert GeoJSON to Esri JSON array

Convert Esri JSON to Esri.Graphic

Inject array into a FeatureLayer

Apply HeatmapRenderer

https://pixabay.com/en/memo-note-yellow-post-is-670689/

Our go-to tools...

AMD Modules

Custom Classes

Web Workers

Promises

 

 https://pixabay.com/en/army-blade-compact-cut-equipment-2186/

var EarthquakeLayer = declare(null, {

    // Custom properties and methods

});

Anonymous Class

SuperClass(es)

Step 1 of 9

           Create our Class file

EarthquakeLayer.js

Step 2 of 9

Define EarthquakeLayer Module

define([
  "dojo/_base/declare",
  "esri/request"
], function(declare, esriRequest) {

  return declare(null, {

      constructor: function(options){

      }
    }
  )
});
define([
  "dojo/_base/declare",
  "esri/request"
], function(declare, esriRequest) {

  return declare(null, {

      constructor: function(options){

      }
    }
  )
});

_base/declare to define a Class

 

define([
  "dojo/_base/declare",
  "esri/request"
], function(declare, esriRequest) {

  return declare(null, {

      constructor: function(options){

      }
    }
  )
});

Create a constructor

define([
  "dojo/_base/declare",
  "esri/request"
], function(declare, esriRequest) {

  return declare(null, {

      constructor: function(options){

      }
    }
  )
});

Include esriRequest for retrieving GeoJSON

Create a constructor options Object

define([
  "dojo/_base/declare",
  "esri/request"
], function(declare, esriRequest) {

  return declare(null, {

      constructor: function(options){

      }
    }
  )
});

Anonymous Class

define([
    "dojo/_base/declare",
    "esri/request"
], function(declare, esriRequest) {

    return declare(null, {
        url: null,

        constructor: function(options){
            this.url = options.url;
        }
    })
});

Add a url property for GeoJSON feed

 

getGeoJSON: function(){
    var geoJSON = esriRequest({
        "url" : this.url
    });

    return geoJSON;
}

Create getGeoJSON()

Retrieve our GeoJSON

Step 3 of 9

Create parseFeatures()

Avoid jank!

parseFeatures: function(featureArray){

    var parseFeaturesDeferred = new Deferred();

    var worker = new Worker("libs/EarthquakeWorker.js");

    return parseFeaturesDeferred.promise;
}

Step 4 of 9

Post/receive messages from worker

worker.postMessage(
    {
        cmd: "parse",
        features: featureArray.features
    }
);

worker.onmessage = function(result){
    worker.terminate();
};

worker.onerror = function(err){
    console.log("Worker error: " + err.message);
};

post GeoJSON

receive esriJSON

A few notes

Worker + JSON size sweetspot

  • Serialization/deserialization
  • Earthquake GeoJSON ~ < 10 MBs
  • Larger JSON files may block UI when passed to a Worker

 

Avoid JSON.parse()

Create Graphics from esriJSON

 

new Graphic(json)
worker.onmessage = function(result){
    worker.terminate();

    var graphicsArr = [];

    for(var i = 0; i < result.data.length; i++){
        var graphic = new Graphic(result.data[i]);
        graphicsArr.push(graphic);
    }

    parseFeaturesDeferred.resolve(graphicsArr);
};

createFeatureLayer()

this.layerDefinition = {
    "objectIdField": "id",
    "geometryType" : "esriGeometryPoint",
    "fields":[{
        "name" : "id",
        "alias" : "id",
        "type" : "esriFieldTypeString"
    },{
        "name" : "depth",
        "alias" : "depth",
        "type": "esriFieldTypeInteger"
    },{
        "name" : "magnitude",
        "alias" : "magnitude",
        "type": "esriFieldTypeDouble"
    }]
};

var featureCollection = {
    "layerDefinition": this.layerDefinition,
    "featureSet": {
        "features": graphicsArr,
        "geometryType": "esriGeometryPoint"
    }
};

Step 5 of 9

Create feature layer from feature collection

Apply HeatmapRenderer

try {
    var featureLayer = new FeatureLayer(featureCollection);

    var heatMapRenderer = new HeatmapRenderer({
        field: "magnitude",
        maxPixelIntensity: 250,
        minPixelIntensity: 10,
        blurRadius: 10
    });

    featureLayer.setRenderer(heatMapRenderer);

    createFeatureLayerDeferred.resolve(featureLayer);
}
catch(err){
    createFeatureLayerDeferred.resolve(false);
}

Create init()

Chain everything together

init: function(){

    var dfd = new Deferred();

    this.getGeoJSON()
        .then(this.parseFeatures)
        .then(this.createFeatureLayer.bind(this))
        .then(function(result){
            dfd.resolve(result);
        });

    return dfd.promise;
},

Step 6 of 9

Create our web worker

EarthquakeWorker.js

Step 7 of 9

Receive GeoJSON (message.data.features)

Parse the features

Post Esri JSON graphics array


onmessage = function(message) { 

    switch(message.data.cmd) {

        case "parse":

            var graphicsArr = parseFeaturesBackground(message.data.features);

            postMessage(graphicsArr);

            break;
    }
};

createParseFeatures()

    var graphicsArray = [];

    for(var i = 0; i < features.length; i++){

        try {
            var graphicJson = {
                "geometry":{
                    "x":features[i].geometry.coordinates[0],
                    "y":features[i].geometry.coordinates[1],
                    "spatialReference":{"wkid":4326}
                },
                "attributes":{
                    "id" : features[i].id,
                    "depth" : features[i].geometry.coordinates[2],
                    "magnitude" : features[i].properties.mag
                }
            };

            graphicsArray.push(graphicJson);
        }
        catch(error){
            console.log("Error creating graphic: " + error);
        }
    }

    return graphicsArray;

Step 8 of 9

In index.html create new EarthquakeLayer

 


map = new Map("map", {
    basemap: "topo",  
    center: [-122.45, 37.75], 
    zoom: 3
});

esriConfig.defaults.io.corsEnabledServers.push("earthquake.usgs.gov");

var earthquakeLayer = new EarthquakeLayer({
    url: "http://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_month.geojson"
});

earthquakeLayer.init().then(function(layer){
    map.addLayer(layer);
});

Step 9 of 9

https://pixabay.com/en/graduate-graduation-school-student-150373/

https://pixabay.com/en/new-year-new-year-s-day-fireworks-152044/

Possible future enhancements

  • Move earthquake retrieval to background thread
  • Re-use a single webworker for parsing
  • Make heat map properties public
  • Include configurable, web-worker background timer to auto-retrieve feed

 

Additional Resources

Andy Gup

agup@esri.com

@agup

 

John Gravois

jgravois@esri.com

@geogangster

Advanced Techniques - ArcGIS API for JavaScript

By Andy G

Advanced Techniques - ArcGIS API for JavaScript

  • 1,736