Map/GnafAddressGeocoder.js

"use strict";

import i18next from "i18next";
/*global require*/
var VarType = require("../Map/VarType");
var AddressGeocoder = require("../Map/AddressGeocoder");
var BulkAddressGeocoderResult = require("../Map/BulkAddressGeocoderResult");
var DeveloperError = require("terriajs-cesium/Source/Core/DeveloperError")
  .default;
var GnafApi = require("../Models/GnafApi");
var defined = require("terriajs-cesium/Source/Core/defined").default;
var JulianDate = require("terriajs-cesium/Source/Core/JulianDate").default;

/**
 * Australia-only address converter, which uses the GNAF api to determine lat and long coordinates for a given address.
 * If TableStructure has address column, adds two new columns: lat and long.
 *
 * @alias GnafAddressGeocoder
 * @constructor
 * @extends AddressGeocoder
 *
 */
var GnafAddressGeocoder = function() {
  AddressGeocoder.call(this);
};

/**
 * Convert addresses in miniumum number of calls to the server. When the promise fulfills, the tableStructure has been
 * updated with new lat and lon columns.
 *
 * @param {TableStructure} [tableStructure] A tableStructure that contains an address column.
 * @param {CorsProxy} [corsProxy] Proxy for cross origin resource sharing
 * @return {Promise} Promise that resolves to an BulkAddressGeocoderResult object.
 */
GnafAddressGeocoder.prototype.bulkConvertAddresses = function(
  tableStructure,
  corsProxy
) {
  this.startTime = JulianDate.now();
  this.tableStructure = tableStructure;
  if (!tableStructure.hasAddress) {
    throw new DeveloperError(i18next.t("map.gnafAddressGeocoder.noAddresses"));
  }

  var addressesCol = tableStructure.columnsByType[VarType.ADDR][0];
  if (addressesCol.length === 0) {
    throw new DeveloperError(i18next.t("map.gnafAddressGeocoder.noAddresses2"));
  }
  var suburbs = tableStructure.getColumnWithName(
    i18next.t("map.gnafAddressGeocoder.suburb")
  );
  smooshColumn(addressesCol, suburbs);
  var state = tableStructure.getColumnWithName(
    i18next.t("map.gnafAddressGeocoder.state")
  );
  smooshColumn(addressesCol, state);
  var postCodeCol = tableStructure.getColumnWithName(
    i18next.t("map.gnafAddressGeocoder.postcode")
  );
  smooshColumn(addressesCol, postCodeCol);
  var gnafApi = new GnafApi(corsProxy);

  var addressesPlusInd = prefilterAddresses(addressesCol.values);
  this.skipIndices = addressesPlusInd.skipIndices;
  this.addressesCol = addressesCol;
  this.numberOfAddressesConverted = addressesPlusInd.addresses.length;
  var that = this;

  return gnafApi
    .bulkGeoCode(addressesPlusInd.addresses, undefined)
    .then(function(info) {
      var longValues = [];
      var latValues = [];
      var matchedAddresses = [];
      var resultScores = [];
      var missingAddresses = [];
      var j = 0;
      for (var i = 0; i < that.addressesCol.values.length; i++) {
        if (
          that.skipIndices.indexOf(i) !== -1 ||
          !defined(info[j]) ||
          !defined(info[j].location) ||
          isNaN(info[j].location.longitude) ||
          isNaN(info[j].location.latitude)
        ) {
          if (that.addressesCol.values[i] !== null) {
            missingAddresses.push(that.addressesCol.values[i]);
          }
          longValues.push(null);
          latValues.push(null);
          resultScores.push(null);
          matchedAddresses.push(null);
          continue;
        }
        longValues.push(info[j].location.longitude);
        latValues.push(info[j].location.latitude);
        resultScores.push(info[j].score);
        matchedAddresses.push(info[j].name);
        j++;
      }
      that.tableStructure.addColumn(
        i18next.t("map.gnafAddressGeocoder.postcode"),
        matchedAddresses
      );
      that.tableStructure.addColumn(
        i18next.t("map.gnafAddressGeocoder.lon"),
        longValues
      );
      that.tableStructure.addColumn(
        i18next.t("map.gnafAddressGeocoder.lat"),
        latValues
      );
      that.tableStructure.addColumn(
        i18next.t("map.gnafAddressGeocoder.score"),
        resultScores
      );
      var addressGeocoderData = new BulkAddressGeocoderResult(
        that.startTime,
        that.numberOfAddressesConverted,
        addressesPlusInd.nullAddresses,
        missingAddresses
      );
      return addressGeocoderData;
    });
};

/**
 * Add info to address, comma separated.
 * @param {TableColumn} [addressesCol] Table column with addresses to be added to
 * @param {TableColumn} [newCol] Table column with extra info to be added to addresses column
 *
 * @private
 */
function smooshColumn(addressesCol, newCol) {
  if (!defined(newCol)) {
    return;
  }
  if (newCol.values.length === addressesCol.values.length) {
    for (var i = 0; i < addressesCol.values.length; i++) {
      if (addressesCol.values[i] !== null && newCol.values[i] !== null) {
        addressesCol.values[i] =
          addressesCol.values[i] + " " + newCol.values[i];
      }
    }
  }
}

/**
 * Do not try to geocode addresses that don't look valid.
 *
 * @param {Array} addressList List of addresses that will be considered for geocoding
 * @return {Object} Probably shorter list of addresses that should be geocoded, as well as indices of addresses that
 *                  were removed.
 * @private
 */
function prefilterAddresses(addressList) {
  var addressesPlusInd = { skipIndices: [], nullAddresses: 0, addresses: [] };

  for (var i = 0; i < addressList.length; i++) {
    var address = addressList[i];
    if (address === null) {
      addressesPlusInd.skipIndices.push(i);
      addressesPlusInd.nullAddresses++;
      continue;
    }
    if (
      address
        .toLowerCase()
        .indexOf(i18next.t("map.gnafAddressGeocoder.poBox")) !== -1 ||
      address
        .toLowerCase()
        .indexOf(i18next.t("map.gnafAddressGeocoder.postOfficeBox")) !== -1
    ) {
      addressesPlusInd.skipIndices.push(i);
      continue;
    }
    addressesPlusInd.addresses.push(address);
  }
  return addressesPlusInd;
}

module.exports = GnafAddressGeocoder;