Source: geoda-proxy.js

/* eslint-disable no-console */
/* eslint-disable no-param-reassign */
// author: lixun910@gmail.com
// date: 10/7/2020 version 0.0.4
// date: 5/14/2021 version 0.0.8

import GeoDaLisa from './geoda-lisa';
import GeoDaWeights from './geoda-weights';

/**
 * Use jsgeoda.New() to get an instance of GeoDaWasm. See New() {@link New}
 * @see New
 * @class
 * @classdesc GeoDaWasm is a class that wraps all the APIs of libgeoda WASM.
 * Always use jsgeoda.{@link New}() to get an instance of GeoDaWasm.
 */
export default class GeoDaWasm {
  /**
   * Should not be called directy.
   * Always use jsgeoda.New() to get an instance of GeoDaWasm.
   * @constructs GeoDaWasm
   * @param {Object} wasm The object of libgeoda WASM
   */
  constructor(wasm) {
    this.version = '0.0.8';
    this.wasm = wasm;
    this.geojson_maps = {};
  }

  /**
   * Help function: create a unique id for a Geojson map
   * @returns {String}
   */
  static generateUid() {
    const result = [];
    const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    const charactersLength = characters.length;
    for (let i = 0; i < 12; i += 1) {
      result.push(characters.charAt(Math.floor(Math.random() * charactersLength)));
    }
    return `map-${result.join('')}`;
  }

  /**
   * Read a geojson map from a file object in the format of ArrayBuffer.
   * You can use {@link readFileSync} in fs to read the geojson file and
   * return a {@link ArrayBuffer};
   * Or use FileReader.{@link readAsArrayBuffer} to read the content of a
   * specified {@link Blob} of {@link File}.
   * @example
   * // In node.js
   * const fs = require('fs');
   * const jsgeoda = require('jsgeoda');
   * const geoda = await jsgeoda.New();
   *
   * let ab = fs.readFileSync("NAT.geojson").buffer;
   * let nat = geoda.read_geojson(ab);
   * let numObs = geoda.get_numobs(nat);
   *
   * E.g. the geojson file name.
   * @param {ArrayBuffer} ab The content of the geojson file in format of ArrayBuffer.
   * @returns {String} A unique id of the geoda object.
   */
  readGeoJSON(ab) {
    const mapUid = GeoDaWasm.generateUid();

    // evt.target.result is an ArrayBuffer
    const uint8Arr = new Uint8Array(ab);
    // First we need to allocate the wasm memory.

    // eslint-disable-next-line no-underscore-dangle
    const uint8Ptr = this.wasm._malloc(uint8Arr.length);
    // Now that we have a block of memory we can copy the file data into that block
    this.wasm.HEAPU8.set(uint8Arr, uint8Ptr);
    // pass the address of the this.wasm memory we just allocated to our function
    this.wasm.new_geojsonmap(mapUid, uint8Ptr, uint8Arr.length);
    // this.wasm.ccall("new_geojsonmap", null, ["string", "number", "number"],
    // [mapUid, uint8_t_ptr, uint8_t_arr.length]);
    // Lastly, according to the docs, we should call ._free here.

    // eslint-disable-next-line no-underscore-dangle
    this.wasm._free(uint8Ptr);

    // store the map and map type
    const mapType = this.wasm.get_map_type(mapUid);
    this.geojson_maps[mapUid] = mapType;

    return mapUid;
  }

  // eslint-disable-next-line camelcase
  read_geojson(a) {
    return this.readGeoJSON(a);
  }

  /**
   * Get map type
   * @param {String} mapUid A unique map id
   * @returns {Number} return map type
   */
  getMapType(mapUid) {
    if (!this.has(mapUid)) {
      // eslint-disable-next-line no-console
      console.log('mapUid is not recognized: ', mapUid);
      return -1;
    }
    return this.geojson_maps[mapUid];
  }

  /**
   * Deprecated!! Read from shapefile: .shp/.dbf/.shx
   * @param {String} mapUid A unique map id
   * @param {ArrayBuffer} data
   * @returns {String}
   */
  /*
  readShapefile(mapUid, data) {
    const uint8Shp = new Uint8Array(data.shp);
    const uint8Dbf = new Uint8Array(data.dbf);
    const uint8Shx = new Uint8Array(data.shx);
    // canread, canwrite, canown
    this.wasm.FS_createDataFile('/', `${mapUid}.shp`, uint8Shp, true, true, true);
    this.wasm.FS_createDataFile('/', `${mapUid}.dbf`, uint8Dbf, true, true, true);
    this.wasm.FS_createDataFile('/', `${mapUid}.shx`, uint8Shx, true, true, true);
    this.wasm.new_shapefilemap(mapUid);
    return mapUid;
  }
  */

  /**
   * Check if a geojson map has been read into GeoDaWasm.
   * @param {String} mapUid A unique map id
   * that has been read into GeoDaWasm.
   * @returns {Boolean} Returns True if the geojson map has been read. Otherwise, returns False.
   */
  has(mapUid) {
    return mapUid in this.geojson_maps;
  }

  /**
   * Free the memory used by wasm
   */
  free() {
    this.wasm.free_geojsonmap();
  }

  /**
   * Check if map uid is valid
   * @param {String} mapUid
   * @returns {Boolean}
   */
  checkMapUid(mapUid) {
    if (!this.has(mapUid)) {
      console.log('mapUid is not recognized: ', mapUid);
      return false;
    }
    return true;
  }

  /**
   * Get map bounds
   * @param {String} mapUid A unique map id
   * that has been read into GeoDaWasm.
   * @returns {Array}
   */
  getBounds(mapUid) {
    if (!this.checkMapUid(mapUid)) return null;
    const bounds = this.wasm.get_bounds(mapUid);
    return GeoDaWasm.parseVecDouble(bounds);
  }

  /**
   * Get viewport for e.g. Deck.gl or GoogleMaps
   * @param {String} mapUid A unique map id
   * @param {Number} mapHeight The height of map (screen pixel)
   * @param {Number} mapWidth The width of map (screen pixel)
   * @returns {Object}
   */
  getViewport(mapUid, mapHeight, mapWidth) {
    if (!this.checkMapUid(mapUid)) return null;

    const bounds = this.getBounds(mapUid);

    const WORLD_DIM = { height: 256, width: 256 };
    const ZOOM_MAX = 21;

    function latRad(lat) {
      const sin = Math.sin((lat * Math.PI) / 180);
      const radX2 = Math.log((1 + sin) / (1 - sin)) / 2;
      return Math.max(Math.min(radX2, Math.PI), -Math.PI) / 2;
    }

    function zoom(mapPx, worldPx, fraction) {
      return Math.floor(Math.log(mapPx / worldPx / fraction) / Math.LN2);
    }

    const ne = { lng: bounds[1], lat: bounds[2] };// .getNorthEast();
    const sw = { lng: bounds[0], lat: bounds[3] };// .getSouthWest();

    const latFraction = Math.abs(latRad(ne.lat) - latRad(sw.lat)) / Math.PI;

    const lngDiff = ne.lng - sw.lng;
    const lngFraction = ((lngDiff < 0) ? (lngDiff + 360) : lngDiff) / 360;

    const mapDim = Math.min(mapHeight, mapWidth);
    const latZoom = zoom(mapDim, WORLD_DIM.height, latFraction);
    const lngZoom = zoom(mapDim, WORLD_DIM.width, lngFraction);

    const z = Math.min(latZoom, lngZoom, ZOOM_MAX);

    return {
      longitude: (bounds[0] + bounds[1]) / 2.0,
      latitude: (bounds[2] + bounds[3]) / 2.0,
      zoom: z - 1,
    };
  }

  /**
   * Get the centroids of geojson map.
   * Same as GEOS.algorithm.Centroid: the centroid were computed as
   * a weighted sum of the centroids of a decomposition of the area
   * into (possibly overlapping) triangles. The algorithm has been
   * extended to handle holes and multi-polygons
   * @example
   * // In node.js
   * const fs = require('fs');
   * const jsgeoda = require('jsgeoda');
   * const geoda = await jsgeoda.New();
   *
   * let ab = fs.readFileSync("NAT.geojson").buffer;
   * let nat = geoda.read_geojson("NAT", ab);
   * let cent = geoda.get_centroids(nat);
   *
   * @param {String} mapUid A unique map id
   * @returns {Array} Returns an array of [x,y] coordinates (not projected) of the centroids.
   */
  getCentroids(mapUid) {
    if (!this.checkMapUid(mapUid)) return null;
    const cc = this.wasm.get_centroids(mapUid);
    const xx = cc.get_x();
    const yy = cc.get_y();
    const centroids = [];
    for (let i = 0; i < xx.size(); i += 1) {
      centroids.push([xx.get(i), yy.get(i)]);
    }
    return centroids;
  }

  /**
   * Get the number of observations or rows in the geojson map.
   * @param {String} mapUid A unique map id
   * @returns {Number} Returns the number of observations or rows in the geojson map.
   */
  getNumberObservations(mapUid) {
    if (!this.checkMapUid(mapUid)) return null;
    const n = this.wasm.get_num_obs(mapUid);
    return n;
  }

  // eslint-disable-next-line camelcase
  getNumObs(mapUid) {
    return this.getNumberObservations(mapUid);
  }

  // eslint-disable-next-line camelcase
  get_numobs(mapUid) {
    return this.getNumberObservations(mapUid);
  }

  /**
   * Get the column names of the geojson map
   * @param {String} mapUid  A unique map id.
   * @returns {Array} Returns the column names
   */
  getColumnNames(mapUid) {
    const names = this.wasm.get_col_names(mapUid);
    return GeoDaWasm.parseVecString(names);
  }

  // eslint-disable-next-line camelcase
  get_colnames(mapUid) {
    return this.getColumnNames(mapUid);
  }

  /**
   * Get the values (numeric|string) of a column or field.
   * @param {String} mapUid  A unique map id.
   * @param {String} colName A string of column or field name.
   * @returns {Array} Returns the values of a column of field.
   */
  getColumn(mapUid, colName) {
    if (!this.checkMapUid(mapUid)) return null;
    const isNumeric = this.wasm.is_numeric_col;
    if (isNumeric) {
      const vals = this.wasm.get_numeric_col(mapUid, colName);
      return GeoDaWasm.parseVecDouble(vals);
    }
    const vals = this.wasm.get_string_col(mapUid, colName);
    return GeoDaWasm.parseVecString(vals);
  }

  getCol(m, c) {
    return this.getColumn(m, c);
  }

  // eslint-disable-next-line camelcase
  get_col(m, c) {
    return this.getColumn(m, c);
  }

  /**
   * Create a Rook contiguity weights.
   * @param {String} mapUid  A unique map id.
   * @param {Number} order An integet number for order of contiguity
   * @param {Boolean} includeLowerOrder Indicate if include lower order when creating weights
   * @param {Number} precisionThreshold Used when the precision of the underlying shape file is
   * insufficient to allow for an exact match of coordinates to determine which polygons
   * are neighbors.
   * @returns {Object} An instance of {@link GeoDaWeights}
   */
  getRookWeights(mapUid, order, includeLowerOrder, precisionThreshold) {
    if (!this.checkMapUid(mapUid)) return null;

    if (order == null) order = 1;
    if (includeLowerOrder == null) includeLowerOrder = false;
    if (precisionThreshold == null) precisionThreshold = 0.0;

    const w = this.wasm.rook_weights(mapUid, order, includeLowerOrder, precisionThreshold);
    return new GeoDaWeights(w);
  }

  /**
   * Create a contiguity weights.
   * @param {String} mapUid  A unique map id.
   * @param {Number} order An integet number for order of contiguity
   * @param {Boolean} includeLowerOrder Indicate if include lower order when creating weights
   * @param {Number} precision_threshold Used when the precision of the underlying shape file
   * is insufficient to allow for an exact match of coordinates to determine which polygons
   * are neighbors.
   * @returns {Object} An instance of {@link GeoDaWeights}
   */
  getQueenWeights(mapUid, order, includeLowerOrder, precision) {
    if (!this.checkMapUid(mapUid)) return null;

    if (order == null) order = 1;
    if (includeLowerOrder == null) includeLowerOrder = false;
    if (precision == null) precision = 0.0;

    const w = this.wasm.queen_weights(mapUid, order, includeLowerOrder, precision);
    return new GeoDaWeights(w);
  }

  /**
   * Get a distance that guarantees that every observation has at least 1 neighbor.
   * @param {String} mapUid  A unique map id.
   * @param {Boolean} isArc  A bool flag indicates if compute arc distance (true) or
   * Euclidean distance (false).
   * @param {Boolean} isMile A bool flag indicates if the distance unit is mile (true)
   * or km (false).
   * @returns {Number}
   */
  getMinDistanceThreshold(mapUid, isArc, isMile) {
    if (!this.checkMapUid(mapUid)) return null;

    if (isArc == null) isArc = false;
    if (isMile == null) isMile = true;

    const val = this.wasm.min_distance_threshold(mapUid, isArc, isMile);
    return val;
  }

  /**
   * Create a K-Nearest Neighbors weights.
   * @param {String} mapUid  A unique map id.
   * @param {Number} k A positive integer number for k-nearest neighbors
   * @param {Number} power  The power (or exponent) indicates how many times to use the number
   * in a multiplication.
   * @param {Boolean} isInverse A bool flag indicates whether or not to apply inverse on
   * distance value.
   * @param {Boolean} isArc  A bool flag indicates if compute arc distance (true) or Euclidean
   * distance (false).
   * @param {Boolean} isMile A bool flag indicates if the distance unit is mile (true) or
   * km (false).
   * @returns {Object} An instance of {@link GeoDaWeights}
   */
  getKnnWeights(mapUid, k, power, isInverse, isArc, isMile) {
    if (!this.checkMapUid(mapUid)) return null;

    if (power == null) power = 1.0;
    if (isInverse == null) isInverse = false;
    if (isArc == null) isArc = false;
    if (isMile == null) isMile = true;

    const w = this.wasm.knn_weights(mapUid, k, power, isInverse, isArc, isMile);
    return new GeoDaWeights(w);
  }

  /**
   * Create a Distance-based weights.
   * @param {String} mapUid A unique map id.
   * @param {Number} distThreshold A positive numeric value of distance threshold
   * used to find neighbors.
   * @param {Number} power  The power (or exponent) indicates how many times to use
   * the number in a multiplication.
   * @param {Boolean} isInverse A bool flag indicates whether or not to apply inverse
   * on distance value.
   * @param {Boolean} isArc  A bool flag indicates if compute arc distance (true) or
   * Euclidean distance (false).
   * @param {Boolean} isMile A bool flag indicates if the distance unit is mile (true)
   * or km (false).
   * @returns {Object} An instance of {@link GeoDaWeights}
   */
  getDistanceWeights(mapUid, distThreshold, power, isInverse, isArc, isMile) {
    if (!this.checkMapUid(mapUid)) return null;

    if (power == null) power = 1.0;
    if (isInverse == null) isInverse = false;
    if (isArc == null) isArc = false;
    if (isMile == null) isMile = true;

    const w = this.wasm.dist_weights(mapUid, distThreshold, power, isInverse, isArc, isMile);
    return new GeoDaWeights(w);
  }

  /**
   * Create a (adaptive) KNN kernel weights.
   *
   * @param {String} mapUid A unique map id.
   * @param {Number} k A positive integer number for k-nearest neighbors
   * @param {String} kernel The name of the kernel function, which could be one of the following:
   * {triangular, uniform, quadratic, epanechnikov, quartic, gaussian}
   * @param {Boolean} adaptiveBandwidth A bool flag indicates whether to use adaptive bandwidth
   * or the max distance of all observation to their k-nearest neighbors.
   * @param {Boolean} useKernelDiagonals A bool flag indicates whether or not the lower order
   * neighbors should be included in the weights structure.
   * @param {Number} power  The power (or exponent) indicates how many times to use the number
   * in a multiplication.
   * @param {Boolean} isInverse A bool flag indicates whether or not to apply inverse on
   * distance value.
   * @param {Boolean} isArc  A bool flag indicates if compute arc distance (true) or Euclidean
   * distance (false).
   * @param {Boolean} isMile A bool flag indicates if the distance unit is mile (true) or
   * km (false)
   * @returns {Object} An instance of {@link GeoDaWeights}
   */
  getKernelKnnWeights(mapUid, k, kernel, adaptiveBandwidth, useKernelDiagonals, power,
    isInverse, isArc, isMile) {
    if (!this.checkMapUid(mapUid)) return null;
    if (!GeoDaWasm.checkInputKernel(kernel)) return null;

    if (useKernelDiagonals == null) useKernelDiagonals = false;
    if (power == null) power = 1.0;
    if (isInverse == null) isInverse = false;
    if (isArc == null) isArc = false;
    if (isMile == null) isMile = true;

    const w = this.wasm.kernel_weights(mapUid, k, kernel, adaptiveBandwidth, useKernelDiagonals,
      power, isInverse, isArc, isMile);

    return new GeoDaWeights(w);
  }

  /**
   * check if input kernel is valid
   *
   * @param {*} kernel
   * @returns {Boolean}
   */
  static checkInputKernel(kernel) {
    if (!(kernel in {
      triangular: true, uniform: true, epanechnikov: true, quartic: true, gaussian: true,
    })) {
      console.log("kernel has to be one of  {'triangular', 'uniform', 'epanechnikov', 'quartic', 'gaussian'}");
      return false;
    }
    return true;
  }

  /**
   * Create a kernel weights with fixed bandwidth.
   *
   * @param {String} mapUid A unique map id.
   * @param {Number} bandwidth The bandwidth (distance threshold).
   * @param {String} kernel The name of the kernel function, which could be one of the following:
   * {triangular, uniform, quadratic, epanechnikov, quartic, gaussian}
   * @param {Boolean} adaptive_bandwidth A bool flag indicates whether to use adaptive bandwidth
   * or the max distance of all observation to their k-nearest neighbors.
   * @param {Boolean} useKernelDiagonals A bool flag indicates whether or not the lower order
   * neighbors should be included in the weights structure.
   * @param {Number} power  The power (or exponent) indicates how many times to use the number
   * in a multiplication.
   * @param {Boolean} isInverse A bool flag indicates whether or not to apply inverse on
   * distance value.
   * @param {Boolean} isArc  A bool flag indicates if compute arc distance (true) or Euclidean
   * distance (false).
   * @param {Boolean} isMile A bool flag indicates if the distance unit is mile (true) or
   * km (false).
   * @returns {Object} An instance of {@link GeoDaWeights}
   */
  getKernelWeights(mapUid, bandwidth, kernel, useKernelDiagonals, power, isInverse,
    isArc, isMile) {
    if (!this.checkMapUid(mapUid)) return null;
    if (!GeoDaWasm.checkInputKernel(kernel)) return null;

    if (useKernelDiagonals === null) useKernelDiagonals = false;
    if (power == null) power = 1.0;
    if (isInverse == null) isInverse = false;
    if (isArc == null) isArc = false;
    if (isMile == null) isMile = true;

    const w = this.wasm.kernel_bandwidth_weights(mapUid, bandwidth, kernel,
      useKernelDiagonals, power, isInverse, isArc, isMile);

    return new GeoDaWeights(w);
  }

  /**
   * Get neighbors (indices) of an observation.
   *
   * @param {String} weights The weights object {@link WeightsResult}
   * @param {Number} idx An integer number represents the index of which observation
   * to get its neighbors.
   * @returns {Array} The indices of neighbors.
   */
  getNeighbors(weights, idx) {
    const mapUid = weights.getMapUid();
    const wUid = weights.getUid();

    const nbrs = this.wasm.get_neighbors(mapUid, wUid, idx);
    return GeoDaWasm.parseVecInt(nbrs);
  }

  /**
   * Get connectivity graph from a weights object
   *
   * @param {String} weights The weights object {@link WeightsResult}
   * @returns {Object} {arcs, targets, sources}
   */
  getConnectivity(weights) {
    const mapUid = weights.getMapUid();

    const centroids = this.getCentroids(mapUid);
    const numobs = this.getNumberObservations(mapUid);
    const arcs = [];
    const targets = [];
    const sources = [];

    for (let i = 0; i < numobs; i += 1) {
      const nbrs = this.getNeighbors(weights, i);
      for (let j = 0; j < nbrs.length; j += 1) {
        const nn = nbrs[j];
        // add point at arc source
        sources.push({
          position: centroids[nn],
          target: centroids[i],
          name: String(j),
          radius: 1,
          gain: 0,
        });
        // add arc
        arcs.push({
          target: centroids[i],
          source: centroids[nn],
          value: 3,
        });
      }
      // add point at arc target
      targets.push({
        position: centroids[i],
        name: String(i),
      });
    }
    return { arcs, targets, sources };
  }

  /**
   * Help function: check if number is an integer.
   *
   * @param {Number} n
   * @returns {Boolean}
   */
  static isInt(n) {
    return Number(n) === n && n % 1 === 0;
  }

  /**
   * Help function: convert GeoDa std::vector to javascript Array e.g. []
   * @param {Array} input
   * @returns {Object}
   */
  static parseVecInt(vi) {
    const result = [];
    for (let j = 0; j < vi.size(); j += 1) {
      result.push(vi.get(j));
    }
    return result;
  }

  /**
   * Help function: convert GeoDa 2d std::vector to javascript 2d Array e.g. [[]]
   * @param {Array} input
   * @returns {Object}
   */
  static parseVecVecInt(vvi) {
    const result = [];
    for (let i = 0; i < vvi.size(); i += 1) {
      const sub = [];
      const vi = vvi.get(i);
      for (let j = 0; j < vi.size(); j += 1) {
        sub.push(vi.get(j));
      }
      result.push(sub);
    }
    return result;
  }

  /**
   * Help function: convert GeoDa 2d std::vector to javascript 2d Array e.g. [[]]
   * @param {Array} input
   * @returns {Object}
   */
  static parseVecVecDouble(vvd) {
    const result = [];
    for (let i = 0; i < vvd.size(); i += 1) {
      const sub = [];
      const vd = vvd.get(i);
      for (let j = 0; j < vd.size(); j += 1) {
        sub.push(vd.get(j));
      }
      result.push(sub);
    }
    return result;
  }

  /**
   * Help function: convert GeoDa std::vector to javascript Array e.g. []
   * @param {Array} input
   * @returns {Object}
   */
  static parseVecDouble(vd) {
    const result = [];
    for (let i = 0; i < vd.size(); i += 1) {
      result.push(vd.get(i));
    }
    return result;
  }

  /**
   * Help function: convert GeoDa std::vector to javascript Array e.g. []
   * @param {Array} input
   * @returns {Object}
   */
  static parseVecString(vd) {
    const result = [];
    for (let i = 0; i < vd.size(); i += 1) {
      result.push(vd.get(i));
    }
    return result;
  }

  /**
   * Help function: convert javascript Array e.g. [] to GeoDa std::vector
   * @param {Array} input
   * @returns {Object}
   */
  toVecString(input) {
    const vs = new this.wasm.VectorString();
    for (let i = 0; i < input.length; i += 1) {
      vs.push_back(input[i]);
    }
    return vs;
  }

  /**
   * Help function: convert javascript Array e.g. [] to GeoDa std::vector
   * @param {Array} input
   * @returns {Object}
   */
  toVecInt(input) {
    const vs = new this.wasm.VectorInt();
    for (let i = 0; i < input.length; i += 1) {
      vs.push_back(input[i]);
    }
    return vs;
  }

  /**
   * Help function: convert javascript Array e.g. [] to GeoDa std::vector
   * @param {Array} input
   * @returns {Object}
   */
  toVecDouble(input) {
    const vs = new this.wasm.VectorDouble();
    for (let i = 0; i < input.length; i += 1) {
      if (Number.isNaN(input[i]) || input[i] === Infinity) {
        vs.push_back(0);
      } else {
        vs.push_back(input[i]);
      }
    }
    return vs;
  }

  /**
   * Help function: convert javascript 2d Array e.g. [[]] to GeoDa 2d std::vector
   * @param {Array} input
   * @returns {Object}
   */
  toVecVecDouble(input) {
    const vvs = new this.wasm.VecVecDouble();
    const iis = new this.wasm.VecVecInt();

    for (let i = 0; i < input.length; i += 1) {
      const vs = new this.wasm.VectorDouble();
      const is = new this.wasm.VectorInt();

      for (let j = 0; j < input[i].length; j += 1) {
        if (Number.isNaN(input[i][j]) || input[i][j] === Infinity) {
          vs.push_back(0);
          is.push_back(1);
        } else {
          vs.push_back(input[i][j]);
          is.push_back(0);
        }
      }
      vvs.push_back(vs);
      iis.push_back(is);
    }
    return { values: vvs, undefs: iis };
  }

  /**
   * Natural breaks
   *
   * @param {Number} k Number of breaks
   * @param {Array} values The values that the classify algorithm will be applied on.
   * @returns {Array} Returns an array of break point values.
   */
  naturalBreaks(k, values) {
    const undefs = values.map((v) => Number.isNaN(v));
    const brks = this.wasm.natural_breaks(k, this.toVecDouble(values),
      this.toVecInt(undefs));
    return GeoDaWasm.parseVecDouble(brks);
  }

  // eslint-disable-next-line camelcase
  natural_breaks(k, v) {
    return this.naturalBreaks(k, v);
  }

  /**
   * Quantile breaks
   *
   * @param {Number} k The number of breaks
   * @param {Array} values The values of selected variable.
   * @returns {Array} Returns an array of break point values.
   */
  quantileBreaks(k, values) {
    const undefs = values.map((v) => Number.isNaN(v));
    const brks = this.wasm.quantile_breaks(k, this.toVecDouble(values),
      this.toVecInt(undefs));
    return GeoDaWasm.parseVecDouble(brks);
  }

  // eslint-disable-next-line camelcase
  quantile_breaks(k, v) {
    return this.quantileBreaks(k, v);
  }

  /**
  * Percentile breaks
  *
  * @param {Array} values The values of selected variable.
  * @returns {Array} Returns an array of break point values.
  */
  percentileBreaks(values) {
    const undefs = values.map((v) => Number.isNaN(v));
    const brks = this.wasm.percentile_breaks(this.toVecDouble(values),
      this.toVecInt(undefs));
    return GeoDaWasm.parseVecDouble(brks);
  }

  /**
  * Standard deviation breaks
  *
  * @param {Array} values The values of selected variable.
  * @returns {Array} Returns an array of break point values.
  */
  standardDeviationBreaks(values) {
    const undefs = values.map((v) => Number.isNaN(v));
    const brks = this.wasm.stddev_breaks(this.toVecDouble(values), this.toVecInt(undefs));
    return GeoDaWasm.parseVecDouble(brks);
  }

  // eslint-disable-next-line camelcase
  stddev_breaks(values) {
    return this.standardDeviationBreaks(values);
  }

  /**
   * Boxplot (hinge=1.5) breaks, including the top, bottom, median, and two quartiles of the data
   *
   * @param {Array} values The values of selected variable.
   * @returns {Array} Returns an array of break point values.
   */
  hinge15Breaks(values) {
    const undefs = values.map((v) => Number.isNaN(v));
    const brks = this.wasm.hinge15_breaks(this.toVecDouble(values),
      this.toVecInt(undefs));
    return GeoDaWasm.parseVecDouble(brks);
  }

  // eslint-disable-next-line camelcase
  hinge15_breaks(v) {
    return this.hinge15Breaks(v);
  }

  /**
   * Boxplot (hinge=3.0) breaks, including the top, bottom, median, and two quartiles of the data
   *
   * @param {Array} values The values of selected variable.
   * @returns {Array} Returns an array of break point values.
   */
  hinge30Breaks(values) {
    const undefs = values.map((v) => Number.isNaN(v));
    const brks = this.wasm.hinge30_breaks(this.toVecDouble(values),
      this.toVecInt(undefs));
    return GeoDaWasm.parseVecDouble(brks);
  }

  // eslint-disable-next-line camelcase
  hinge30_breaks(v) {
    return this.hinge30Breaks(v);
  }

  /**
   * Custom breaks that wraps {'natural_breaks', 'quantile_breaks', 'stdDevBreaks',
   * 'hinge15Breaks', 'hinge30Breaks'}
   *
   * @param {String} breakName The break name: {'natural_breaks', 'quantile_breaks',
   * 'stdDevBreaks', 'hinge15Breaks', 'hinge30Breaks'}
   * @param {*} values The values of selected variable.
   * @param {*} k The number of breaks.
   * @returns {Object} {'k','bins','breaks','id_array'}
   */
  customBreaks(breakName, values, k) {
    let breaks = [];
    if (breakName === 'naturalBreaks') {
      breaks = this.naturalBreaks(k, values);
    } else if (breakName === 'quantileBreaks') {
      breaks = this.quantileBreaks(k, values);
    } else if (breakName === 'percentileBreaks') {
      breaks = this.percentileBreaks(values);
    } else if (breakName === 'standardDeviationBreaks') {
      breaks = this.standardDeviationBreaks(values);
    } else if (breakName === 'hinge15Breaks') {
      breaks = this.hinge15Breaks(values);
    } else if (breakName === 'hinge30Breaks') {
      breaks = this.hinge30Breaks(values);
    } else {
      return null;
    }
    const origBreaks = breaks;

    const bins = [];
    const idArray = [];
    for (let i = 0; i < breaks.length; i += 1) {
      idArray.push([]);
      const txt = GeoDaWasm.isInt(breaks[i]) ? breaks[i] : breaks[i].toFixed(2);
      bins.push(`${txt}`);
    }
    idArray.push([]);
    let txt = breaks[breaks.length - 1];
    if (txt !== undefined) {
      txt = GeoDaWasm.isInt(txt) ? txt : txt.toFixed(2);
      bins.push(`> ${txt}`);
    }

    breaks.unshift(Number.NEGATIVE_INFINITY);
    breaks.push(Number.POSITIVE_INFINITY);

    for (let i = 0; i < values.length; i += 1) {
      const v = values[i];
      for (let j = 0; j < breaks.length - 1; j += 1) {
        const minVal = breaks[j];
        const maxVal = breaks[j + 1];
        if (v >= minVal && v < maxVal) {
          idArray[j].push(i);
          break;
        }
      }
    }

    return {
      k,
      bins,
      breaks: origBreaks,
      id_array: idArray,
    };
  }

  /**
   * Excess Risk
   *
   * @param {Array} eventValues The values of an event variable.
   * @param {Array} baseValues  The values of an base variable.
   * @returns {Array}
   */
  excessRisk(eventValues, baseValues) {
    const r = this.wasm.excess_risk(this.toVecDouble(eventValues),
      this.toVecDouble(baseValues));
    return GeoDaWasm.parseVecDouble(r);
  }

  /**
   * Empirical Bayes (EB) Smoothed Rate
   *
   * @param {Array} eventValues The values of an event variable.
   * @param {Array} baseValues  The values of an base variable.
   * @returns {Array}
   */
  empiricalBayesRisk(eventValues, baseValues) {
    const r = this.wasm.eb_risk(this.toVecDouble(eventValues),
      this.toVecDouble(baseValues));
    return GeoDaWasm.parseVecDouble(r);
  }

  /**
   * Compute spatially lagged variable.
   *
   * @param {WeightsResult} weights The weights object {@link WeightsResult}
   * @param {Array} values The values of a selected variable.
   * @param {Boolean} isBinary The bool value indicates if the spatial weights
   * is used as binary weights. Default: TRUE.
   * @param {Boolean} rowStandardize The bool value indicates if use
   * row-standardized weights. Default: TRUE
   * @param {Bollean} includeDiagonal The bool value indicates if include
   * diagonal of spatial weights. Default: FALSE
   * @returns {Array}
   */
  spatialLag(weights, values, isBinary, rowStandardize, includeDiagonal) {
    const mapUid = weights.getMapUid();
    const wUid = weights.getUid();
    const data = this.toVecDouble(values);

    if (isBinary == null) isBinary = true;
    if (rowStandardize == null) rowStandardize = true;
    if (includeDiagonal == null) includeDiagonal = false;

    const r = this.wasm.spatial_lag(mapUid, wUid, data, isBinary,
      rowStandardize, includeDiagonal);
    return GeoDaWasm.parseVecDouble(r);
  }

  /**
   * Spatial rate smoothing
   *
   * @param {WeightsResult} weights The weights object {@link WeightsResult}
   * @param {Array} eventValues The values of an event variable.
   * @param {Array} baseValues  The values of an base variable.
   * @returns {Array}
   */
  spatialRate(weights, eventValues, baseValues) {
    const mapUid = weights.getMapUid();
    const wUid = weights.getUid();

    const r = this.wasm.spatial_rate(this.toVecDouble(eventValues),
      this.toVecDouble(baseValues), mapUid, wUid);
    return GeoDaWasm.parseVecDouble(r);
  }

  /**
   * Spatial Empirical Bayes (EB) Smoothing
   *
   * @param {WeightsResult} weights The weights object {@link WeightsResult}
   * @param {Array} eventValues The values of an event variable.
   * @param {Array} baseValues  The values of an base variable.
   * @returns {Array}
   */
  spatialEmpiricalBayes(weights, eventValues, baseValues) {
    const mapUid = weights.getMapUid();
    const wUid = weights.getUid();

    const r = this.wasm.spatial_eb(this.toVecDouble(eventValues),
      this.toVecDouble(baseValues), mapUid, wUid);
    return GeoDaWasm.parseVecDouble(r);
  }

  /**
   * Create cartogram using the values in the map.
   * In cartograms, the size of a variable's value corresponds to the size of a shape.
   * The location of the circles is aligned as closely as possible to the location of
   * the associated area through a nonlinear optimization routine
   * @param {String} mapUid A unique map Id
   * @param {Array} values The values that the classify algorithm will be applied on.
   * @returns {Array} Returns an array of circles, which is defined as:
   * {
   *    "properties": { "id" : 1},
   *    "position": [0.01, 0.01],
   *    "radius": 0.1
   * }
   */
  cartogram(mapUid, values) {
    const cart = this.wasm.cartogram(mapUid, this.toVecDouble(values));
    const x = cart.get_x();
    const y = cart.get_y();
    const r = cart.get_radius();
    // rescale x, y [-100,0], [0, 45]
    let minX = x.get(0);
    let maxX = x.get(0);
    let minY = y.get(0);
    let maxY = y.get(0);
    for (let i = 0; i < x.size(); i += 1) {
      if (minX > x.get(i)) minX = x.get(i);
      if (maxX < x.get(i)) maxX = x.get(i);
      if (minY > y.get(i)) minY = y.get(i);
      if (maxY < y.get(i)) maxY = y.get(i);
    }
    // const scaleX = 100.0 / (maxX - minX);
    // const scaleY = 45.0 / (maxY - minY);

    const result = [];
    for (let i = 0; i < x.size(); i += 1) {
      // let xx = (x.get(i) - minX) * scaleX;
      // let yy = (y.get(i) - minY) * scaleY;
      result.push({
        properties: {
          id: i,
        },
        position: [x.get(i) / 10000.0, y.get(i) / 10000.0],
        radius: r.get(i),
      });
    }
    return result;
  }

  /**
   * Apply local Moran statistics
   *
   * @param {String} weights The weights object {@link WeightsResult}
   * @param {Array} values The values that local moran statistics will be applied on.
   * @param {Number} permutations The number of permutations for the LISA computation. Default: 999.
   * * @param {String} permutationMethod The permutation method used for the LISA computation.
   * Options are 'complete', 'lookup'. Default: 'lookup'.
   * @param {Number} seed The seed for random number generator used in LISA statistics.
   * Default: 123456789.
   * @returns {Object} An instance of {@link LisaResult}
   */
  localMoran(weights, values, permutations, permutationMethod, significanceCutoff, seed) {
    return this.callLisa('localMoran', weights, values, permutations, permutationMethod, significanceCutoff, seed);
  }

  /**
   * Apply local G statistics
   *
   * @param {String} weights The weights object {@link WeightsResult}
   * @param {Array} values The values that local moran statistics will be applied on.
   * @param {Number} permutations the number of permutations for the LISA computation. Default: 999.
   * * @param {String} permutationMethod The permutation method used for the LISA computation.
   * Options are 'complete', 'lookup'. Default: 'lookup'.
   * @param {Number} seed The seed for random number generator used in LISA statistics.
   * Default: 123456789.
   * @returns {Object} An instance of {@link LisaResult}
   */
  localG(weights, values, permutations, permutationMethod, significanceCutoff, seed) {
    return this.callLisa('localG', weights, values, permutations, permutationMethod, significanceCutoff, seed);
  }

  /**
   * Apply local G* statistics
   *
   * @param {String} weights The weights object {@link WeightsResult}
   * @param {Array} values The values that local moran statistics will be applied on.
   * @param {Number} permutations the number of permutations for the LISA computation. Default: 999.
   * * @param {String} permutationMethod The permutation method used for the LISA computation.
   * Options are 'complete', 'lookup'. Default: 'lookup'.
   * @param {Number} seed The seed for random number generator used in LISA statistics.
   * Default: 123456789.
   * @returns {Object} An instance of {@link LisaResult}
   */
  localGStar(weights, values, permutations, permutationMethod, significanceCutoff, seed) {
    return this.callLisa('localGStar', weights, values, permutations, permutationMethod, significanceCutoff, seed);
  }

  /**
   * Apply local Geary statistics
   *
   * @param {String} weights The weights object {@link WeightsResult}
   * @param {Array} values The values that local moran statistics will be applied on.
   * @param {Number} permutations the number of permutations for the LISA computation. Default: 999.
   * @param {String} permutationMethod The permutation method used for the LISA computation.
   * Options are 'complete', 'lookup'. Default: 'lookup'.
   * @param {Number} seed The seed for random number generator used in LISA statistics.
   * Default: 123456789.
   * @returns {Object} An instance of {@link LisaResult}
   */
  localGeary(weights, values, permutations, permutationMethod, significanceCutoff, seed) {
    return this.callLisa('localGeary', weights, values, permutations, permutationMethod, significanceCutoff, seed);
  }

  /**
   * Apply local Join Count statistics
   *
   * @param {String} weights The weights object {@link WeightsResult}
   * @param {Array} values The values that local moran statistics will be applied on.
   * @param {Number} permutations the number of permutations for the LISA computation. Default: 999.
   * * @param {String} permutationMethod The permutation method used for the LISA computation.
   * Options are 'complete', 'lookup'. Default: 'lookup'.
   * @param {Number} seed The seed for random number generator used in LISA statistics.
   * Default: 123456789.
   * @returns {Object} An instance of {@link LisaResult}
   */
  localJoinCount(weights, values, permutations, permutationMethod, significanceCutoff, seed) {
    return this.callLisa('localJoinCount', weights, values, permutations, permutationMethod, significanceCutoff, seed);
  }

  /**
   * Apply Quantile LISA statistics
   *
   * @param {String} weights The weights object {@link WeightsResult}
   * @param {Array} values The values that local moran statistics will be applied on.
   * @param {Number} permutations the number of permutations for the LISA computation. Default: 999.
   * * @param {String} permutationMethod The permutation method used for the LISA computation.
   * Options are 'complete', 'lookup'. Default: 'lookup'.
   * @param {Number} seed The seed for random number generator used in LISA statistics.
   * Default: 123456789.
   * @returns {Object} An instance of {@link LisaResult}
   */
  quantileLisa(weights, k, quantile, values, permutations, permutationMethod,
    significanceCutoff, seed) {
    const mapUid = weights.getMapUid();
    const weightUid = weights.getUid();

    if (permutations == null) permutations = 999;
    if (permutationMethod == null) permutationMethod = 'lookup';
    if (significanceCutoff == null) significanceCutoff = 0.05;
    if (seed == null) seed = 123456789;

    if (!(permutationMethod in { lookup: true, complete: true })) {
      console.log("Permutation method needs to be one of {'lookup', 'complete'}.");
      return null;
    }

    const undefs = values.map((v) => Number.isNaN(v));
    const undefsVec = this.toVecInt(undefs);
    const vals = this.toVecDouble(values);

    const lisaObj = this.wasm.quantile_lisa(mapUid, weightUid, k, quantile, vals, undefsVec,
      significanceCutoff, permutations, permutationMethod, seed);
    return lisaObj !== null ? new GeoDaLisa(lisaObj, this) : null;
  }

  /**
   * Helper function: apply LISA statistics
   *
   * @param {String} weights The weights object {@link WeightsResult}
   * @param {Array} values The values that local moran statistics will be applied on.
   * @param {Number} permutations The number of permutations for the LISA computation. Default: 999.
   * * @param {String} permutationMethod The permutation method used for the LISA computation.
   * Options are 'complete', 'lookup'. Default: 'lookup'.
   * @param {Number} seed The seed for random number generator used in LISA statistics.
   * Default: 123456789.
   * @returns {Object} An instance of {@link LisaResult}
   */
  callLisa(lisaFunction, weights, values, permutations, permutationMethod,
    significanceCutoff, seed) {
    const mapUid = weights.getMapUid();
    const weightUid = weights.getUid();

    if (permutations == null) permutations = 999;
    if (permutationMethod == null) permutationMethod = 'lookup';
    if (significanceCutoff == null) significanceCutoff = 0.05;
    if (seed == null) seed = 123456789;

    if (!(permutationMethod in { lookup: true, complete: true })) {
      // eslint-disable-next-line no-undef
      console.log("Permutation method needs to be one of {'lookup', 'complete'}.");
      return null;
    }

    const undefs = values.map((v) => Number.isNaN(v));
    const undefsVec = this.toVecInt(undefs);
    const vals = this.toVecDouble(values);

    let lisaObj = null;
    if (lisaFunction === 'localMoran') {
      lisaObj = this.wasm.local_moran(mapUid, weightUid, vals, undefsVec, significanceCutoff,
        permutations, permutationMethod, seed);
    } else if (lisaFunction === 'localG') {
      lisaObj = this.wasm.local_g(mapUid, weightUid, vals, undefsVec, significanceCutoff,
        permutations, permutationMethod, seed);
    } else if (lisaFunction === 'localGStar') {
      lisaObj = this.wasm.local_gstar(mapUid, weightUid, vals, undefsVec, significanceCutoff,
        permutations, permutationMethod, seed);
    } else if (lisaFunction === 'localGeary') {
      lisaObj = this.wasm.local_geary(mapUid, weightUid, vals, undefsVec, significanceCutoff,
        permutations, permutationMethod, seed);
    } else if (lisaFunction === 'localJoinCount') {
      lisaObj = this.wasm.local_joincount(mapUid, weightUid, vals, undefsVec, significanceCutoff,
        permutations, permutationMethod, seed);
    } else {
      // eslint-disable-next-line no-console
      console.log('lisaFunction is not valid: ', lisaFunction);
      return null;
    }

    return new GeoDaLisa(lisaObj, this);
  }

  /**
   * Help function: Get scale methods.
   *
   * @returns {Object}
   */
  static scaleMethods() {
    return {
      raw: true,
      standardize: true,
      demean: true,
      mad: true,
      range_standardize: true,
      range_adjust: true,
    };
  }

  /**
   * Help function: Get distance methods.
   *
   * @returns {Object}
   */
  static distanceMethods() {
    return {
      euclidean: true,
      manhattan: true,
    };
  }

  /**
   * The local neighbor match test is a method to identify significant locations by assessing
   * the extent of overlap between k-nearest neighbors in geographical space and
   * k-nearest neighbors in multi-attribute space.
   *
   * @param {String} mapUid A unique string represents the geojson map that has been
   * read into GeoDaWasm.
   * @param {Number} knn k nearest neighbor for both attribute and geographical space
   * @param {Array} data The array of numeric columns that contains the values for
   * neighbor match test
   * @param {String} scaleMethod The scaling method:  {'raw', 'standardize', 'demean',
   * 'mad', 'range_standardize', 'range_adjust'}. Default: 'standardize'
   * @param {String} distanceMethod The distance method: {'euclidean', 'manhattan'}.
   * Default: 'euclidean'.
   * @param {Number} power The power/exponent corresponds to the number of times the
   * base (dist_band) is used as a factor. Default: 1.
   * @param {Boolean} isInverse The bool value indicates if apply inverse on distance
   * value. Default: False.
   * @param {Boolean} isArc The bool value indicates if compute arc distance between
   * two observations. Default: FALSE.
   * @param {Boolean} isMile The bool value indicates if convert distance unit from
   * mile to kilometer(KM). Default: TRUE.
   * @returns {Array} {'cardinality', 'probability'}
   */
  neighborMatchTest(mapUid, knn, data, scaleMethod, distanceMethod, power, isInverse,
    isArc, isMile) {
    if (scaleMethod == null) scaleMethod = 'standardize';
    const definedScaleMethods = GeoDaWasm.scaleMethods();
    if (!(scaleMethod in definedScaleMethods)) {
      console.log('The scaling method is not valid.');
      return null;
    }

    if (distanceMethod == null) distanceMethod = 'euclidean';
    const definedDistMethods = GeoDaWasm.distanceMethods();
    if (!(distanceMethod in definedDistMethods)) {
      console.log('The distance method is not valid.');
      return null;
    }
    if (power == null) power = 1.0;
    if (isInverse == null) isInverse = false;
    if (isArc == null) isArc = false;
    if (isMile == null) isMile = true;

    const inData = this.toVecVecDouble(data);
    const r = this.wasm.neighbor_match_test(mapUid, knn, power, isInverse, isArc, isMile,
      inData.values, scaleMethod, distanceMethod);
    const rr = GeoDaWasm.parseVecVecDouble(r);

    return {
      cardinality: rr[0],
      probability: rr[1],
    };
  }

  /**
   * Multivariate local geary is a multivariate extension of local geary
   * which measures the extent to which neighbors in multiattribute space
   * are also neighbors in geographical space.
   *
   * @param {WeightsResult} weights The weights object {@link WeightsResult}
   * @param {Array} values1 The array of the numeric columns that contains the
   * values for LISA statistics
   * @param {Number} permutations The number of permutations for the LISA computation. Default: 999.
   * * @param {String} permutationMethod The permutation method used for the LISA computation.
   * Options are 'complete', 'lookup'. Default: 'lookup'.
   * @param {Number} significanceCutoff The cutoff value for significance p-values to filter
   * not-significant clusters. Default: 0.05
   * @param {Number} seed The seed for random number generator
   * @returns {Object} LISA object {@link GeoDaLisa}
   */
  localMultiGeary(weights, values, permutations, permutationMethod, significanceCutoff, seed) {
    const mapUid = weights.getMapUid();
    const weightUid = weights.getUid();

    if (permutations == null) permutations = 999;
    if (permutationMethod == null) permutationMethod = 'lookup';
    if (significanceCutoff == null) significanceCutoff = 0.05;
    if (seed == null) seed = 123456789;

    if (!(permutationMethod in { lookup: true, complete: true })) {
      console.log("Permutation method needs to be one of {'lookup', 'complete'}.");
      return null;
    }

    const data = this.toVecVecDouble(values);
    const lisaObj = this.wasm.local_multigeary(mapUid, weightUid, data.values, data.undefs,
      significanceCutoff, permutations, permutationMethod, seed);

    return lisaObj !== null ? new GeoDaLisa(lisaObj, this) : null;
  }

  /**
   * Bivariate or no-colocation local join count works when two events cannot happen in the
   * same location. It can be used to identify negative spatial autocorrelation.
   *
   * @param {WeightsResult} weights The weights object {@link WeightsResult}
   * @param {Array} values1 The first numeric column that contains the binary values
   * (e.g. 0 and 1) for LISA statistics
   * @param {Array} values2 The second numeric column that contains the binary values
   * (e.g. 0 and 1) for LISA statistics
   * @param {Number} permutations The number of permutations for the LISA computation. Default: 999.
   * * @param {String} permutationMethod The permutation method used for the LISA computation.
   * Options are 'complete', 'lookup'. Default: 'lookup'.
   * @param {Number} significanceCutoff The cutoff value for significance p-values to filter
   * not-significant clusters. Default: 0.05
   * @param {Number} seed The seed for random number generator
   * @returns {Object} LISA object {@link GeoDaLisa}
   */
  localBiJoinCount(weights, values1, values2, permutations, permutationMethod,
    significanceCutoff, seed) {
    const mapUid = weights.getMapUid();
    const weightUid = weights.getUid();

    if (permutations == null) permutations = 999;
    if (permutationMethod == null) permutationMethod = 'lookup';
    if (significanceCutoff == null) significanceCutoff = 0.05;
    if (seed == null) seed = 123456789;

    if (!(permutationMethod in { lookup: true, complete: true })) {
      console.log("Permutation method needs to be one of {'lookup', 'complete'}.");
      return null;
    }

    const numObs = values1.length;
    for (let i = 0; i < numObs; i += 1) {
      if ((values1[i] !== 0 && values1[i] !== 1) || (values2[i] !== 0 && values2[i] !== 1)) {
        console.log('The input data is not binary.');
        return null;
      }
    }

    for (let i = 0; i < numObs; i += 1) {
      if (values1[i] === 1 && values2[i] === 1) {
        console.log('The bivariate local join count only applies on two variables with no-colocation.');
        return null;
      }
    }

    const data = this.toVecVecDouble([values1, values2]);
    const lisaObj = this.wasm.local_multijoincount(mapUid, weightUid, data.values, data.undefs,
      significanceCutoff, permutations, permutationMethod, seed);

    return lisaObj !== null ? new GeoDaLisa(lisaObj, this) : null;
  }

  /**
   * Multivariate or colocation local join count (2019) works when two or more events
   * happen in the same location.
   *
   * @param {WeightsResult} weights The weights object {@link WeightsResult}
   * @param {Array} values The array of numeric columns that contains the binary values
   * (e.g. 0 and 1) for LISA statistics
   * @param {Number} permutations The number of permutations for the LISA computation. Default: 999.
   * * @param {String} permutationMethod The permutation method used for the LISA computation.
   * Options are 'complete', 'lookup'. Default: 'lookup'.
   * @param {Number} significanceCutoff The cutoff value for significance p-values to filter
   * not-significant clusters. Default: 0.05
   * @param {Number} seed The seed for random number generator
   * @returns {Object} LISA object {@link GeoDaLisa}
   */
  localMultiJoinCount(weights, values, permutations, permutationMethod, significanceCutoff, seed) {
    const mapUid = weights.getMapUid();
    const weightUid = weights.getUid();

    if (permutations == null) permutations = 999;
    if (permutationMethod == null) permutationMethod = 'lookup';
    if (significanceCutoff == null) significanceCutoff = 0.05;
    if (seed == null) seed = 123456789;

    if (!(permutationMethod in { lookup: true, complete: true })) {
      console.log("Permutation method needs to be one of {'lookup', 'complete'}.");
      return null;
    }

    const numVars = values.length;
    if (numVars) {
      console.log('The input data is not from multivariate variables.');
      return null;
    }

    const numObs = values[0].length;
    for (let i = 0; i < numVars; i += 1) {
      for (let j = 0; j < numObs; j += 1) {
        if (values[i][j] !== 0 && values[i][j] !== 1) {
          console.log('The input data is not binary.');
          return null;
        }
      }
    }

    if (numVars === 2) {
      for (let i = 0; i < numObs; i += 1) {
        if (values[0][i] === 1 && values[1][i] === 1) {
          console.log('The input two variables have no colocations. Please use: localBiJoinCount().');
          return null;
        }
      }
    }

    const data = this.toVecVecDouble(values);
    const lisaObj = this.wasm.local_multijoincount(mapUid, weightUid, data.values, data.undefs,
      significanceCutoff, permutations, permutationMethod, seed);

    return lisaObj !== null ? new GeoDaLisa(lisaObj, this) : null;
  }

  /**
   * Multivariate Quantile LISA (2019) is a type of local spatial autocorrelation that applies
   * multivariate local join count statistics to quantiles of multiple continuous variables.
   *
   * @param {WeightsResult} weights The weights object {@link WeightsResult}
   * @param {Array} ks The array of integer numbers that specify quantiles for each variable
   * @param {Array} quantiles The array of integer numbers that specify which quantile
   * is used for each variable
   * @param {Array} values The array of numeric columns that contains the binary values
   * (e.g. 0 and 1) for LISA statistics
   * @param {Number} permutations The number of permutations for the LISA computation. Default: 999.
   * * @param {String} permutationMethod The permutation method used for the LISA computation.
   * Options are 'complete', 'lookup'. Default: 'lookup'.
   * @param {Number} significanceCutoff The cutoff value for significance p-values to filter
   * not-significant clusters. Default: 0.05
   * @param {Number} seed The seed for random number generator
   * @returns {Object} LISA object {@link GeoDaLisa}
   */
  multiQuantileLisa(weights, ks, quantiles, values, permutations, permutationMethod,
    significanceCutoff, seed) {
    const mapUid = weights.getMapUid();
    const weightUid = weights.getUid();

    if (permutations == null) permutations = 999;
    if (permutationMethod == null) permutationMethod = 'lookup';
    if (significanceCutoff == null) significanceCutoff = 0.05;
    if (seed == null) seed = 123456789;

    if (!(permutationMethod in { lookup: true, complete: true })) {
      console.log("Permutation method needs to be one of {'lookup', 'complete'}.");
      return null;
    }

    const numVars = values.length;
    if (numVars !== ks.length || numVars !== quantiles.length) {
      console.log('The data size of ks, quantiles and values are not the same.');
      return null;
    }

    const inKs = this.toVecInt(ks);
    const inQuantiles = this.toVecInt(quantiles);
    const data = this.toVecVecDouble(values);

    const lisaObj = this.wasm.multi_quantile_lisa(mapUid, weightUid, inKs, inQuantiles,
      data.values, data.undefs, significanceCutoff, permutations, permutationMethod, seed);
    return lisaObj !== null ? new GeoDaLisa(lisaObj, this) : null;
  }

  /**
   * Helper function: Get REDCAP methods.
   *
   * @returns {Array}
   */
  static redcapMethods() {
    return {
      'firstorder-singlelinkage': true,
      'fullorder-completelinkage': true,
      'fullorder-averagelinkage': true,
      'fullorder-singlelinkage': true,
      'fullorder-wardlinkage': true,
    };
  }

  /**
   * Spatial C(K)luster Analysis by Tree Edge Removal
   *
   * @param {WeightsResult} weights The weights object {@link WeightsResult}
   * @param {Number} k The number of clusters
   * @param {Array} values The list of numeric vectors of selected variable
   * @param {Number} minBound The minimum value that the sum value of bounding
   * variable int each cluster should be greater than
   * @param {Array} boundVals The numeric vector of selected bounding variable
   * @param {String} scaleMethod The scaling method: {'raw', 'standardize', 'demean', 'mad',
   * 'range_standardize', 'range_adjust'}
   * @param {String} distanceMethod The distance method: {"euclidean", "manhattan"}
   * @returns {Object} Return a ClusteringResult object:
   * {'total_ss', 'within_ss', 'between_ss', 'ratio', 'clusters'
   */
  skater(weights, k, values, minBound, boundVals, scaleMethod, distanceMethod) {
    return this.redcap(weights, k, values, 'firstorder-singlelinkage', minBound,
      boundVals, scaleMethod, distanceMethod);
  }

  /**
   * Helper function: check if scale method is valid.
   *
   * @param {String} scaleMethod
   * @returns {Boolean}
   */
  static checkScaleMethod(scaleMethod) {
    const definedScaleMethods = GeoDaWasm.scaleMethods();
    if (!(scaleMethod in definedScaleMethods)) {
      console.log('The scaling method is not valid.');
      return false;
    }
    return true;
  }

  /**
   * Helper function: check if distance method is valid.
   *
   * @param {String} distanceMethod
   * @returns {Boolean}
   */
  static checkDistanceMethod(distanceMethod) {
    const definedDistMethods = GeoDaWasm.distanceMethods();
    if (!(distanceMethod in definedDistMethods)) {
      console.log('The distance method is not valid.');
      return false;
    }
    return true;
  }

  /**
   * Helper function: get clustering results
   *
   * @param {Object} r
   * @returns {Object} {'clusters', 'total_ss', 'between_ss', 'within_ss', 'ratio'}
   */
  static getClusteringResult(r) {
    if (r.is_valid()) {
      return {
        clusters: GeoDaWasm.parseVecInt(r.clusters()),
        total_ss: r.total_ss(),
        between_ss: r.between_ss(),
        within_ss: GeoDaWasm.parseVecDouble(r.within_ss()),
        ratio: r.ratio(),
      };
    }
    return null;
  }

  /**
   * Regionalization with dynamically constrained agglomerative clustering and partitioning (REDCAP)
   *
   * @param {WeightsResult} weights The weights object {@link WeightsResult}
   * @param {Number} k The number of clusters
   * @param {Array} values The list of numeric vectors of selected variable
   * @param {String} method The REDCAP method:
   * {'single-linkage', 'average-linkage', 'complete-linkage', 'Ward-linkage'}.
   * @param {Number} minBound The minimum value that the sum value of bounding variable in each
   * cluster should be greater than
   * @param {Array} boundVals The numeric vector of selected bounding variable
   * @param {String} scaleMethod The scaling method: {'raw', 'standardize', 'demean', 'mad',
   * 'range_standardize', 'range_adjust'}
   * @param {String} distanceMethod The distance method: {"euclidean", "manhattan"}
   * @returns {Object} Return a ClusteringResult object:
   * {'total_ss', 'within_ss', 'between_ss', 'ratio', 'clusters'}
   */
  redcap(weights, k, values, method, minBound, boundVals, scaleMethod, distanceMethod) {
    const redcapMethods = GeoDaWasm.redcapMethods();
    if (!(method in redcapMethods)) {
      console.log('Redcap method is not valid');
      return null;
    }

    if (scaleMethod == null) scaleMethod = 'standardize';
    if (distanceMethod == null) distanceMethod = 'euclidean';
    if (!GeoDaWasm.checkScaleMethod(scaleMethod)) return null;
    if (!GeoDaWasm.checkDistanceMethod(distanceMethod)) return null;

    const mapUid = weights.getMapUid();
    const wUid = weights.getUid();
    const data = this.toVecVecDouble(values);

    if (minBound == null) minBound = 0;
    if (boundVals == null) boundVals = [];

    const r = this.wasm.redcap(mapUid, wUid, k, method, data.values,
      this.toVecDouble(boundVals), minBound, scaleMethod, distanceMethod);
    return GeoDaWasm.getClusteringResult(r);
  }

  /**
   * Get the SCHC methods.
   *
   * @returns {Array}
   */
  static schcMethods() {
    return {
      single: true,
      complete: true,
      average: true,
      ward: true,
    };
  }

  /**
   * Spatially Constrained Hierarchical Clucstering (SCHC)
   *
   * @param {WeightsResult} weights The weights object {@link WeightsResult}
   * @param {Number} k The number of clusters
   * @param {Array} values The list of numeric vectors of selected variable
   * @param {String} method The method of agglomerative hierarchical clustering:
   * {"single", "complete", "average","ward"}.
   * @param {Number} minBound The minimum value that the sum value of bounding variable in each
   * cluster should be greater than
   * @param {Array} boundVals The numeric vector of selected bounding variable
   * @param {String} scaleMethod The scaling method: {'raw', 'standardize', 'demean', 'mad',
   * 'range_standardize', 'range_adjust'}
   * @param {String} distanceMethod The distance method: {"euclidean", "manhattan"}
   * @returns {Object} Return a ClusteringResult object:
   * {'total_ss', 'within_ss', 'between_ss', 'ratio', 'clusters'}
   */
  schc(weights, k, values, method, minBound, boundVals, scaleMethod, distanceMethod) {
    const schcMethods = GeoDaWasm.schcMethods();
    if (!(method in schcMethods)) {
      console.log('schc method is not valid');
      return null;
    }

    if (scaleMethod == null) scaleMethod = 'standardize';
    if (distanceMethod == null) distanceMethod = 'euclidean';
    if (!GeoDaWasm.checkScaleMethod(scaleMethod)) return null;
    if (!GeoDaWasm.checkDistanceMethod(distanceMethod)) return null;

    const mapUid = weights.getMapUid();
    const wUid = weights.getUid();
    const data = this.toVecVecDouble(values);

    if (minBound == null) minBound = 0;
    if (boundVals == null) boundVals = [];

    const r = this.wasm.schc(mapUid, wUid, k, method, data.values,
      this.toVecDouble(boundVals), minBound, scaleMethod, distanceMethod);
    return GeoDaWasm.getClusteringResult(r);
  }

  /**
   * A greedy algorithm to solve the AZP problem
   *
   * @param {WeightsResult} weights The weights object {@link WeightsResult}
   * @param {Number} k The number of spatially constrained clusters
   * @param {Array} values The list of numeric vectors of selected variable.
   * @param {Number} inits The number of construction re-runs, which is for ARiSeL
   * "automatic regionalization with initial seed location"
   * @param {Array} initRegion  The initial regions that the local search starts with.
   * Default is empty. means the local search starts with a random process to "grow" clusters
   * @param {Array} minBoundValues The list of numeric array of selected minimum bounding variables.
   * @param {Array} minBounds The list of minimum value that the sum value of bounding variables
   * in each cluster should be greater than.
   * @param {Array} maxBoundValues The list of numeric array of selected maximum bounding variables.
   * @param {Array} maxBounds The list of minimum value that the sum value of bounding variables
   * in each cluster should be less than.
   * @param {String} scaleMethod The scaling methods {'raw', 'standardize', 'demean', 'mad',
   * 'range_standardize', 'range_adjust'}. Defaults to 'standardize'.
   * @param {String} distanceMethod The distance methods {"euclidean", "manhattan"}.
   * Defaults to 'euclidean'.
   * @param {Number} seed The seed for random number generator.
   * @returns {Object} Return a ClusteringResult object:
   * {'total_ss', 'within_ss', 'between_ss', 'ratio', 'clusters'}
   */
  azpGreedy(weights, k, values, inits, initRegion, minBoundValues, minBounds,
    maxBoundValues, maxBounds, scaleMethod, distanceMethod, seed) {
    if (inits == null) inits = 0;
    if (initRegion == null) initRegion = [];

    if (scaleMethod == null) scaleMethod = 'standardize';
    if (distanceMethod == null) distanceMethod = 'euclidean';
    if (!GeoDaWasm.checkScaleMethod(scaleMethod)) return null;
    if (!GeoDaWasm.checkDistanceMethod(distanceMethod)) return null;

    const mapUid = weights.getMapUid();
    const wUid = weights.getUid();
    const data = this.toVecVecDouble(values);

    if (minBoundValues == null) minBoundValues = [];
    if (minBounds == null) minBounds = [];
    if (maxBoundValues == null) maxBoundValues = [];
    if (maxBounds == null) maxBounds = [];

    const inMinBoundValues = this.toVecVecDouble(minBoundValues).values;
    const inMinBounds = this.toVecDouble(minBounds);
    const inMaxBoundValues = this.toVecVecDouble(maxBoundValues).values;
    const inMaxBounds = this.toVecDouble(maxBounds);

    if (seed == null) seed = 123456789;

    const r = this.wasm.azp_greedy(mapUid, wUid, k, data.values, inits,
      this.toVecInt(initRegion),
      scaleMethod, distanceMethod, inMinBoundValues, inMinBounds, inMaxBoundValues,
      inMaxBounds, seed);
    return GeoDaWasm.getClusteringResult(r);
  }

  /**
   * A simulated annealing algorithm to solve the AZP problem
   *
   * @param {WeightsResult} weights The weights object {@link WeightsResult}
   * @param {Number} k The number of spatially constrained clusters
   * @param {Array} values The list of numeric vectors of selected variable.
   * @param {Number} coolingRate  The number of iterations of simulated annealing. Defaults to 1
   * @param {Number} saMaxIt The number of iterations of simulated annealing. Defaults to 1
   * @param {Number} inits The number of construction re-runs, which is for ARiSeL
   * "automatic regionalization with initial seed location"
   * @param {Array} initRegion  The initial regions that the local search starts with.
   * Default is empty. means the local search starts with a random process to "grow" clusters
   * @param {Array} minBoundValues The list of numeric array of selected minimum bounding variables.
   * @param {Array} minBounds The list of minimum value that the sum value of bounding variables
   * in each cluster should be greater than.
   * @param {Array} maxBoundValues The list of numeric array of selected maximum bounding variables.
   * @param {Array} maxBounds The list of minimum value that the sum value of bounding variables
   * in each cluster should be less than.
   * @param {String} scaleMethod The scaling methods {'raw', 'standardize', 'demean', 'mad',
   * 'range_standardize', 'range_adjust'}. Defaults to 'standardize'.
   * @param {String} distanceMethod The distance methods {"euclidean", "manhattan"}.
   * Defaults to 'euclidean'.
   * @param {Number} seed The seed for random number generator.
   * @returns {Object} Return a ClusteringResult object:
   * {'total_ss', 'within_ss', 'between_ss', 'ratio', 'clusters'}
   */
  azpSA(weights, k, values, coolingRate, saMaxIt, inits, initRegion, minBoundValues,
    minBounds, maxBoundValues, maxBounds, scaleMethod, distanceMethod, seed) {
    if (coolingRate == null) coolingRate = 0.85;
    if (saMaxIt == null) saMaxIt = 1;
    if (inits == null) inits = 0;
    if (initRegion == null) initRegion = [];

    if (scaleMethod == null) scaleMethod = 'standardize';
    if (distanceMethod == null) distanceMethod = 'euclidean';
    if (!GeoDaWasm.checkScaleMethod(scaleMethod)) return null;
    if (!GeoDaWasm.checkDistanceMethod(distanceMethod)) return null;

    const mapUid = weights.getMapUid();
    const wUid = weights.getUid();
    const data = this.toVecVecDouble(values);

    if (minBoundValues == null) minBoundValues = [];
    if (minBounds == null) minBounds = [];
    if (maxBoundValues == null) maxBoundValues = [];
    if (maxBounds == null) maxBounds = [];

    const inMinBoundValues = this.toVecVecDouble(minBoundValues).values;
    const inMinBounds = this.toVecDouble(minBounds);
    const inMaxBoundValues = this.toVecVecDouble(maxBoundValues).values;
    const inMaxBounds = this.toVecDouble(maxBounds);

    if (seed == null) seed = 123456789;

    const r = this.wasm.azp_sa(mapUid, wUid, k, coolingRate, saMaxIt, data.values, inits,
      this.toVecInt(initRegion), scaleMethod, distanceMethod, inMinBoundValues,
      inMinBounds, inMaxBoundValues, inMaxBounds, seed);
    return GeoDaWasm.getClusteringResult(r);
  }

  /**
   * A tabu-search algorithm to solve the AZP problem.
   *
   * @param {WeightsResult} weights The weights object {@link WeightsResult}
   * @param {Number} k The number of spatially constrained clusters
   * @param {Array} values The list of numeric vectors of selected variable.
   * @param {Number} tabuLength The length of a tabu search heuristic of tabu algorithm.
   * Defaults to 10.
   * @param {Number} convTabu The number of non-improving moves. Defaults to 10.
   * @param {Number} inits The number of construction re-runs, which is for ARiSeL
   * "automatic regionalization with initial seed location"
   * @param {Array} initRegion  The initial regions that the local search starts with.
   * Default is empty. means the local search starts with a random process to "grow" clusters
   * @param {Array} minBoundValues The list of numeric array of selected minimum bounding variables.
   * @param {Array} minBounds The list of minimum value that the sum value of bounding variables
   * in each cluster should be greater than.
   * @param {Array} maxBoundValues The list of numeric array of selected maximum bounding variables.
   * @param {Array} maxBounds The list of minimum value that the sum value of bounding variables
   * in each cluster should be less than.
   * @param {String} scaleMethod The scaling methods {'raw', 'standardize', 'demean', 'mad',
   * 'range_standardize', 'range_adjust'}. Defaults to 'standardize'.
   * @param {String} distanceMethod The distance methods {"euclidean", "manhattan"}.
   * Defaults to 'euclidean'.
   * @param {Number} seed The seed for random number generator.
   * @returns {Object} Return a ClusteringResult object:
   * {'total_ss', 'within_ss', 'between_ss', 'ratio', 'clusters'}
   */
  azpTabu(weights, k, values, tabuLength, convTabu, inits, initRegion, minBoundValues, minBounds,
    maxBoundValues, maxBounds, scaleMethod, distanceMethod, seed) {
    if (tabuLength == null) tabuLength = 10;
    if (convTabu == null) convTabu = 10;
    if (inits == null) inits = 0;
    if (initRegion == null) initRegion = [];

    if (scaleMethod == null) scaleMethod = 'standardize';
    if (distanceMethod == null) distanceMethod = 'euclidean';
    if (!GeoDaWasm.checkScaleMethod(scaleMethod)) return null;
    if (!GeoDaWasm.checkDistanceMethod(distanceMethod)) return null;

    const mapUid = weights.getMapUid();
    const wUid = weights.getUid();
    const data = this.toVecVecDouble(values);

    if (minBoundValues == null) minBoundValues = [];
    if (minBounds == null) minBounds = [];
    if (maxBoundValues == null) maxBoundValues = [];
    if (maxBounds == null) maxBounds = [];

    const inMinBoundValues = this.toVecVecDouble(minBoundValues).values;
    const inMinBounds = this.toVecDouble(minBounds);
    const inMaxBoundValues = this.toVecVecDouble(maxBoundValues).values;
    const inMaxBounds = this.toVecDouble(maxBounds);

    if (seed == null) seed = 123456789;

    const r = this.wasm.azp_tabu(mapUid, wUid, k, tabuLength, convTabu, data.values, inits,
      this.toVecInt(initRegion), scaleMethod, distanceMethod, inMinBoundValues, inMinBounds,
      inMaxBoundValues, inMaxBounds, seed);
    return GeoDaWasm.getClusteringResult(r);
  }

  /**
   * A greedy algorithm to solve the max-p-region problem.
   *
   * @param {WeightsResult} weights The weights object {@link WeightsResult}
   * @param {Array} values The list of numeric vectors of selected variable.
   * @param {Number} iterations The number of iterations of greedy algorithm. Defaults to 1.
   * @param {Array} minBoundValues The list of numeric array of selected minimum bounding variables.
   * @param {Array} minBounds The list of minimum value that the sum value of bounding variables
   * in each cluster should be greater than.
   * @param {Array} maxBoundValues The list of numeric array of selected maximum bounding variables.
   * @param {Array} maxBounds The list of minimum value that the sum value of bounding variables
   * in each cluster should be less than.
   * @param {String} scaleMethod The scaling methods {'raw', 'standardize', 'demean', 'mad',
   * 'range_standardize', 'range_adjust'}. Defaults to 'standardize'.
   * @param {String} distanceMethod The distance methods {"euclidean", "manhattan"}.
   * Defaults to 'euclidean'.
   * @param {Number} seed The seed for random number generator
   * @returns {Object} Return a ClusteringResult object:
   * {'total_ss', 'within_ss', 'between_ss', 'ratio', 'clusters'}
   */
  maxpGreedy(weights, values, iterations, minBoundValues, minBounds, maxBoundValues, maxBounds,
    scaleMethod, distanceMethod, seed) {
    if (iterations == null) iterations = 1;

    if (scaleMethod == null) scaleMethod = 'standardize';
    if (distanceMethod == null) distanceMethod = 'euclidean';
    if (!GeoDaWasm.checkScaleMethod(scaleMethod)) return null;
    if (!GeoDaWasm.checkDistanceMethod(distanceMethod)) return null;

    const mapUid = weights.getMapUid();
    const wUid = weights.getUid();
    const data = this.toVecVecDouble(values);

    if (minBoundValues == null || minBounds == null) {
      console.log('maxp needs minBounds and minBoundValues arguments.');
    }

    if (maxBoundValues == null) maxBoundValues = [];
    if (maxBounds == null) maxBounds = [];

    const inMinBoundValues = this.toVecVecDouble(minBoundValues).values;
    const inMinBounds = this.toVecDouble(minBounds);
    const inMaxBoundValues = this.toVecVecDouble(maxBoundValues).values;
    const inMaxBounds = this.toVecDouble(maxBounds);

    if (seed == null) seed = 123456789;

    const r = this.wasm.maxp_greedy(mapUid, wUid, data.values, iterations, scaleMethod,
      distanceMethod, inMinBoundValues, inMinBounds, inMaxBoundValues, inMaxBounds, seed);
    return GeoDaWasm.getClusteringResult(r);
  }

  /**
   * A simulated annealing algorithm to solve the max-p-region problem.
   *
   * @param {WeightsResult} weights The weights object {@link WeightsResult}
   * @param {Array} values The list of numeric vectors of selected variable.
   * @param {Number} coolingRate The cooling rate of a simulated annealing algorithm.
   * Defaults to 0.85
   * @param {Number} saMaxIt The number of iterations of simulated annealing. Defaults to 1
   * @param {Number} iterations The number of iterations of greedy algorithm. Defaults to 1.
   * @param {Array} minBoundValues The list of numeric array of selected minimum bounding variables.
   * @param {Array} minBounds The list of minimum value that the sum value of bounding variables
   * in each cluster should be greater than.
   * @param {Array} maxBoundValues The list of numeric array of selected maximum bounding variables.
   * @param {Array} maxBounds The list of minimum value that the sum value of bounding variables
   * in each cluster should be less than.
   * @param {String} scaleMethod The scaling methods {'raw', 'standardize', 'demean', 'mad',
   * 'range_standardize', 'range_adjust'}. Defaults to 'standardize'.
   * @param {String} distanceMethod The distance methods {"euclidean", "manhattan"}.
   * Defaults to 'euclidean'.
   * @param {Number} seed The seed for random number generator.
   * @returns {Object} Return a ClusteringResult object:
   * {'total_ss', 'within_ss', 'between_ss', 'ratio', 'clusters'}
   */
  maxpSA(weights, values, coolingRate, saMaxIt, iterations, minBoundValues, minBounds,
    maxBoundValues, maxBounds, scaleMethod, distanceMethod, seed) {
    if (coolingRate == null) coolingRate = 0.85;
    if (saMaxIt == null) saMaxIt = 1;
    if (iterations == null) iterations = 1;

    if (scaleMethod == null) scaleMethod = 'standardize';
    if (distanceMethod == null) distanceMethod = 'euclidean';
    if (!GeoDaWasm.checkScaleMethod(scaleMethod)) return null;
    if (!GeoDaWasm.checkDistanceMethod(distanceMethod)) return null;

    const mapUid = weights.getMapUid();
    const wUid = weights.getUid();
    const data = this.toVecVecDouble(values);

    if (minBoundValues == null || minBounds == null) {
      console.log('maxp needs minBounds and minBoundValues arguments.');
    }

    if (maxBoundValues == null) maxBoundValues = [];
    if (maxBounds == null) maxBounds = [];

    const inMinBoundValues = this.toVecVecDouble(minBoundValues).values;
    const inMinBounds = this.toVecDouble(minBounds);
    const inMaxBoundValues = this.toVecVecDouble(maxBoundValues).values;
    const inMaxBounds = this.toVecDouble(maxBounds);

    if (seed == null) seed = 123456789;

    const r = this.wasm.maxp_sa(mapUid, wUid, data.values, iterations, coolingRate,
      saMaxIt, scaleMethod, distanceMethod, inMinBoundValues, inMinBounds,
      inMaxBoundValues, inMaxBounds, seed);
    return GeoDaWasm.getClusteringResult(r);
  }

  /**
   * A tabu-search algorithm to solve the max-p-region problem
   * @param {WeightsResult} weights The weights object {@link WeightsResult}
   * @param {Array} values The list of numeric vectors of selected variable.
   * @param {Number} tabuLength The length of a tabu search heuristic of tabu algorithm.
   * Defaults to 10.
   * @param {Number} convTabu The number of non-improving moves. Defaults to 10.
   * @param {Number} iterations The number of iterations of greedy algorithm. Defaults to 1.
   * @param {Array} minBoundValues The list of numeric array of selected minimum bounding variables.
   * @param {Array} minBounds The list of minimum value that the sum value of bounding variables
   * in each cluster should be greater than.
   * @param {Array} maxBoundValues The list of numeric array of selected maximum bounding variables.
   * @param {Array} maxBounds The list of minimum value that the sum value of bounding variables
   * in each cluster should be less than.
   * @param {String} scaleMethod The scaling methods {'raw', 'standardize', 'demean', 'mad',
   * 'range_standardize', 'range_adjust'}. Defaults to 'standardize'.
   * @param {String} distanceMethod The distance methods {"euclidean", "manhattan"}.
   * Defaults to 'euclidean'.
   * @param {Number} seed The seed for random number generator.
   * @returns {Object} Return a ClusteringResult object:
   * {'total_ss', 'within_ss', 'between_ss', 'ratio', 'clusters'}
   */
  maxpTabu(weights, values, tabuLength, convTabu, iterations, minBoundValues, minBounds,
    maxBoundValues, maxBounds, scaleMethod, distanceMethod, seed) {
    if (tabuLength == null) tabuLength = 10;
    if (convTabu == null) convTabu = 10;
    if (iterations == null) iterations = 1;

    if (scaleMethod == null) scaleMethod = 'standardize';
    if (distanceMethod == null) distanceMethod = 'euclidean';
    if (!GeoDaWasm.checkScaleMethod(scaleMethod)) return null;
    if (!GeoDaWasm.checkDistanceMethod(distanceMethod)) return null;

    const mapUid = weights.getMapUid();
    const wUid = weights.getUid();
    const data = this.toVecVecDouble(values);

    if (minBoundValues == null) minBoundValues = [];
    if (minBounds == null) minBounds = [];
    if (maxBoundValues == null) maxBoundValues = [];
    if (maxBounds == null) maxBounds = [];

    const inMinBoundValues = this.toVecVecDouble(minBoundValues).values;
    const inMinBounds = this.toVecDouble(minBounds);
    const inMaxBoundValues = this.toVecVecDouble(maxBoundValues).values;
    const inMaxBounds = this.toVecDouble(maxBounds);

    if (seed == null) seed = 123456789;

    const r = this.wasm.maxp_tabu(mapUid, wUid, data.values, iterations, tabuLength,
      convTabu, scaleMethod, distanceMethod, inMinBoundValues, inMinBounds,
      inMaxBoundValues, inMaxBounds, seed);
    return GeoDaWasm.getClusteringResult(r);
  }
}