Build a custom async

AMD module

Use Case:

Real-time earthquake heatmap

Advanced Tools

AMD Modules

Custom Classes

Promises

Extend

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

Advanced Tools

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/

Custom

AMD Modules

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

 

Named Class

declare("namespace.ClassName", null, {

    // Custom properties and methods

});
declare("namespace.ClassName", null, {

    // Custom properties and methods

});

Name and namespace of Class

SuperClass(es)

var MyClass = declare(null, {

    // Custom properties and methods

});

Anonymous Class

SuperClass(es)

Build a custom

real-time

earthquake map

TO-DO List

Retrieve the GeoJSON data

Convert GeoJSON to Graphics array

Inject array into a FeatureLayer

Apply HeatmapRenderer

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

Step 1 of 7

Create a file called

EarthquakeLayer.js

Note: Starts with capitalized letter

Step 2 of 7

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){

      }
    }
  )
});

Register the module

Tells loader to automatically load dependencies

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

  return declare(null, {

      constructor: function(options){

      }
    }
  )
});

_base/declare to define a Class via Prototypical Inheritance

 

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

  return declare(null, {

      constructor: function(options){

      }
    }
  )
});

Create a constructor - fires when Class is instantiated

 

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){

      }
    }
  )
});

Null value signifies no inheritance

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;
}

Retrieve GeoJSON feed

esriRequest returns a Promise

Step 3 of 7

Parse the GeoJSON features

  • Turn features into a Graphics Array
  • Could have hundreds features
  • Need to avoid jank!
  • Parse GeoJSON in the background - use Web Worker!

 

Step 4 of 7

Initialize primary promise

Return primary promise

Initialize primary promise

Return primary promise

Parse features on background thread

Send to web worker

Parse features

Create array of Json Graphics

Return Json array to main thread

Initialize primary promise

Return primary promise

Parse features on background thread

Resolve primary promise

Return Json array to main thread

Convert Esri Json to Esri Graphics

Create parseFeatures()

Initialize web worker

Return a promise

parseFeatures: function(featureArray){

    var dfd = new Deferred();

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

    return dfd.promise;
}

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

Resolve primary Promise

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);
    }

    dfd.resolve(graphicsArr);
};

Create feature collection using array

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"
    }
};

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);

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

Chain it all together in init()

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 5 of 7

Create web worker named

EarthquakeWorker.js

Step 6 of 7

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;
    }
};

Parse the features on background thread


function parseFeaturesBackground(features){

    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;
}

In index.html create new EarthquakeLayer

Run init()

Add feature layer to map


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 7 of 7

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 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 (full)

By Andy G

Advanced Techniques - ArcGIS API for JavaScript (full)

  • 1,816