Map/RegionProviderList.js

"use strict";

/*global require*/
var defined = require("terriajs-cesium/Source/Core/defined").default;
var loadJson = require("../Core/loadJson");
var DeveloperError = require("terriajs-cesium/Source/Core/DeveloperError")
  .default;

var RegionProvider = require("./RegionProvider");

/**********************************************************************************************
*  Region mapping support - turn CSVs into choropleths of regions like local government areas
*  Implements most of https://github.com/TerriaJS/nationalmap/wiki/csv-geo-au
*  How this works:
*  1. If there are 'lat' and 'lon' columns, use those instead to generate a point display.
*  2. Look through all the column names in the CSV file until we find one that matches a defined region descriptor in
      region mapping JSON file (not included in TerriaJS)
*  3. Contact the WFS server defined in that file, fetch IDs for all regions of that type
*  4. Based on values in the region variable column, generate a linear choropleth mapping
*  5. Fetch specially prepared WMS tiles which are coloured with a unique colour per region.
*  6. Recolor each tile, using the unique colour to determine its ID, then replacing that unique colour with a mapped color.
*
* VoilĂ  - very fast client-side choroplething with no user data sent to servers, and no vector boundaries sent to client. */

/**
 * RegionProviderList encapsulates the regionMapping.json file and provides support for choosing the best region
 * provider for a given dataset.
 *
 * @alias RegionProviderList
 * @constructor
 * @param corsProxy A proxy to allow the region providers to make AJAX calls safely.
 */
var RegionProviderList = function(corsProxy) {
  /**
   * List of RegionProviders, once loaded from file.
   * @type {RegionProvider[]}
   */
  this.regionProviders = [];

  // To be passed to new RegionProviders
  this.corsProxy = corsProxy;
};

/* A static dictionary of promises to RegionProviderLists */
RegionProviderList.metaList = {};

/**
 * Returns a promise for a RegionProviderList instantiated from this url. Previous loads are cached.
 *
 * @param {String} url
 * @returns {Promise} A promise for a RegionProviderList instantiated from this url.
 */
RegionProviderList.fromUrl = function(url, corsProxy) {
  if (!defined(RegionProviderList.metaList[url])) {
    RegionProviderList.metaList[url] = loadJson(url).then(function(o) {
      return new RegionProviderList(corsProxy).initFromObject(o);
    });
  }
  return RegionProviderList.metaList[url];
};

/**
 * Initialises from the already-retrieved contents of a JSON file.
 *
 * @param {Object} obj
 */
RegionProviderList.prototype.initFromObject = function(obj) {
  var that = this;
  Object.keys(obj.regionWmsMap).forEach(function(r) {
    that.regionProviders.push(
      new RegionProvider(r, obj.regionWmsMap[r], that.corsProxy)
    );
  });
  // after loading all providers, now we can set cross references for disambiguation, where required.
  Object.keys(obj.regionWmsMap).forEach(function(r) {
    var rp = that.getRegionProvider(r);
    if (obj.regionWmsMap[r].disambigRegionId) {
      rp.setDisambigProperties(
        that.getRegionProvider(obj.regionWmsMap[r].disambigRegionId)
      );
    }
  });
  return this;
};

/**
 * Find what kind of region-mapped dataset this is by exhaustively looking for every alias for every variable name provided.
 * You can optionally provide a prefered region variable name, and optionally its prefered type.
 * If specified, getRegionDetails will return that name as the first object in the returned array.
 *
 * @param variableNames {String[]} Array of names of variables to choose amongst.
 * @param variableNames {String[]} Array of names of variables to choose amongst.
 * @return {Object[]} An array of objects with regionProvider (RegionProvider), regionVariable and disambigVariable properties, for each potential region variable in the list of names.
 */
RegionProviderList.prototype.getRegionDetails = function(
  variableNames,
  preferedRegionVariableName,
  preferedRegionType
) {
  var results = [];
  // If preferedRegionVariableName is in variableNames (which it must be to be meaningful; if undefined, it won't be), remove it first.
  var index = variableNames.indexOf(preferedRegionVariableName);
  if (index >= 0) {
    variableNames = variableNames.slice(); // clone it so we don't potentially confuse the caller.
    variableNames.splice(index, 1);
    if (!defined(preferedRegionType)) {
      // If no type is provided, simply put this back in the front of the array, to prioritize it.
      variableNames.unshift(preferedRegionVariableName);
    } else {
      // If a type is provided, handle it specially.
      var regionProvider = this.getRegionProvider(preferedRegionType);
      results.push({
        regionProvider: regionProvider,
        variableName: preferedRegionVariableName,
        disambigVariableName: regionProvider.findDisambigVariable(variableNames)
      });
    }
  }
  // Figure out the region variable and type based on aliases.
  for (var i = 0; i < this.regionProviders.length; i++) {
    var rv = this.regionProviders[i].findRegionVariable(variableNames);
    if (rv) {
      results.push({
        regionProvider: this.regionProviders[i],
        variableName: rv,
        disambigVariableName: this.regionProviders[i].findDisambigVariable(
          variableNames
        )
      });
    }
  }
  return results;
};

/**
 * Get the region provider matching a regionType string (eg, STE)
 *
 * @param {String} regionType The type string to look up.
 * @return {RegionProvider} The region provider matching a regionType string (eg, STE), or undefined.
 */
RegionProviderList.prototype.getRegionProvider = function(regionType) {
  var r = this.regionProviders.filter(function(p) {
    return p.regionType.toLowerCase() === regionType.toLowerCase();
  });
  if (r.length > 1) {
    throw new DeveloperError(
      "More than one definition of region provider: " + regionType
    );
  }
  if (r.length === 0) {
    return undefined;
  }
  return r[0];
};

module.exports = RegionProviderList;