Source: josm/ds.mjs

/**
 * Provides utitly methods for data sets
 *
 * @module josm/ds
 */

/* global Java */

import * as util from 'josm/util'
import { NodeBuilder, WayBuilder, RelationBuilder } from 'josm/builder'
export const DataSet = Java.type('org.openstreetmap.josm.data.osm.DataSet')
const SimplePrimitiveId = Java.type('org.openstreetmap.josm.data.osm.SimplePrimitiveId')
const PrimitiveId = Java.type('org.openstreetmap.josm.data.osm.PrimitiveId')
export const OsmPrimitiveType = Java.type('org.openstreetmap.josm.data.osm.OsmPrimitiveType')
const Collection = Java.type('java.util.Collection')
const HashSet = Java.type('java.util.HashSet')
const File = Java.type('java.io.File')
const FileWriter = Java.type('java.io.FileWriter')
const PrintWriter = Java.type('java.io.PrintWriter')
const FileInputStream = Java.type('java.io.FileInputStream')
const OsmImporter = Java.type('org.openstreetmap.josm.gui.io.importexport.OsmImporter')
const OsmChangeImporter = Java.type('org.openstreetmap.josm.gui.io.importexport.OsmChangeImporter')
const OsmReader = Java.type('org.openstreetmap.josm.io.OsmReader')
const OsmChangeReader = Java.type('org.openstreetmap.josm.io.OsmChangeReader')
const Utils = Java.type('org.openstreetmap.josm.tools.Utils')
const GZIPInputStream = Java.type('java.util.zip.GZIPInputStream')
const OsmWriterFactory = Java.type('org.openstreetmap.josm.io.OsmWriterFactory')
const Changeset = Java.type('org.openstreetmap.josm.data.osm.Changeset')
const System = Java.type('java.lang.System')

function log (msg) {
  System.out.println(msg)
}

function normalizeType (type) {
  if (util.isString(type)) {
    type = type.trim().toLowerCase()
    if ('node'.startsWith(type)) {
      return OsmPrimitiveType.NODE
    } else if ('way'.startsWith(type)) {
      return OsmPrimitiveType.WAY
    } else if ('relation'.startsWith(type)) {
      return OsmPrimitiveType.RELATION
    } else {
      util.assert(false,
        'expected type as string, i.e. "node", "way", or "relation", got "{0}"',
        type)
    }
  } else if (type instanceof OsmPrimitiveType) {
    return type
  } else {
    util.assert(false, 'expected String or OsmPrimitiveType, got "{0}", type')
  }
}

function normalizeId (id) {
  if (util.isNumber(id)) {
    if (Number.isInteger(id)) {
      return id
    } else {
      util.assert(false, 'expected integer , got "{0}"', id)
    }
  } else if (util.isString(id)) {
    const idSaved = id
    id = parseInt(id.trim())
    if (isNaN(id)) {
      util.assert(false, 'expected integer as string, got "{0}"', idSaved)
    }
  } else {
    util.assert(false, 'expected an integer or a string, got "{0}"', id)
  }
}

/**
* Creates an ID for an OSM primitive.
*
* <strong>Signatures</strong>
* <dl>
*   <dt><code class="signature">buildId(id, type)</code></dt>
*   <dd class="param-desc">Replies an object given by its unique numeric id and a type.
*   The type is either a string <code>node</code>, <code>way</code>, or
*   <code>relation</code>, or one of the symbols
*   {@class org.openstreetmap.josm.data.osm.OsmPrimitiveType}.NODE,
*   {@class org.openstreetmap.josm.data.osm.OsmPrimitiveType}.WAY, or
*   {@class org.openstreetmap.josm.data.osm.OsmPrimitiveType}.RELATION.</dd>
*
*   <dt><code class="signature">buildId(id)</code></dt>
*   <dd class="param-desc">Replies an object given an ID. <code>id</code> is either an instance
*   of
*   {@class org.openstreetmap.josm.data.osm.PrimitiveId} or an object with
*   the properties <code>id</code> and <code>type</code>, i.e.
*   <code>{id: 1234, type: 'node'}</code>.</dd>
* </dl>
*
* @example
* import { buildId, OsmPrimitiveType} from 'josm/ds'
*
* // build a node id
* const id1 = buildId(1234, 'node')
*
* // build a way id
* const id2 = buildId(3333, OsmPrimitiveType.WAY)
*
* // build a relation id
* const id3 = buildId({id: 5423, type: 'relation'})
*
*
* @param args see description
*/
export function buildId (id, type) {
  function buildId2 (id, type) {
    id = normalizeId(id)
    type = normalizeType(type)
    if (id === 0) {
      util.assert(false, 'expected id != 0, got 0')
    }
    return new SimplePrimitiveId(id, type)
  }

  function buildId1 (id) {
    if (id instanceof PrimitiveId) {
      return id
    }
    if (util.hasProp(id, 'id') && util.hasProp(id, 'type')) {
      return buildId2(id.id, id.type)
    }
    util.assert(false, 'expected PrimitiveId or {id: ..., type: ...}, got "{0}"', id)
  }

  util.assert(arguments.length > 0, 'expected at least 1 argument, got 0')
  switch (arguments.length) {
    case 1:
      return buildId1(...arguments)
    case 2:
      return buildId2(...arguments)
    default:
      util.assert(false, 'expected 1 or 2 arguments, got {0}', arguments.length)
  }
}

function each (collection, delegate) {
  if (util.isArray(collection) || util.isArguments(collection)) {
    for (let i = 0; i < collection.length; i++) {
      delegate(collection[i])
    }
  } else if (collection instanceof Collection) {
    for (let it = collection.iterator(); it.hasNext();) {
      delegate(it.next())
    }
  } else {
    util.assert(false, 'Expected list or collection, got {0}', collection)
  }
}

function collect (collection, predicate) {
  const ret = []
  each(collection, (obj) => {
    if (predicate(obj)) ret.push(obj)
  })
  return ret
}

function isCollection (collection) {
  return util.isArray(collection) ||
    util.isArguments(collection) ||
    collection instanceof Collection
}

function normalizeIds () {
  function walk (set, ids) {
    if (util.isNothing(ids)) return
    if (ids instanceof PrimitiveId) {
      set.add(ids)
    } else if (isCollection(ids)) {
      each(ids, (that) => walk(set, that))
    } else {
      util.assert(false,
        'PrimitiveId or collection required, got {0}', ids)
    }
  }
  const set = new HashSet()
  for (let i = 0; i < arguments.length; i++) {
    walk(set, arguments[i])
  }
  return set
}

/**
 * <code>DataSetUtil</code> provides methods to build OSM primitive IDs and to
 * manipulate data in a {@class org.openstreetmap.josm.data.osm.DataSet}.
 *
 */
export class DataSetUtil {
  /**
   * Creates an instance of <code>DataSetUtil</code> for a given {@class org.openstreetmap.josm.data.osm.DataSet}
   *
   * @example
   * import { DataSetUtil, DataSet } from 'josm/ds'
   * const dsutil = new DataSetUtil(new DataSet())
   *
   * @summary Build an utility object wrapping the dataset <code>ds</code>
   * @param {org.openstreetmap.josm.data.osm.DataSet} [ds] the dataset. Creates a new dataset if missing
   */
  constructor (ds) {
    ds = ds || new DataSet()
    this.ds = ds
  }

  /**
   * Replies an OSM object from the dataset, or undefined, if no such object
   * exists.
   *
   * <strong>Signatures</strong>
   * <dl>
   *   <dt><code class="signature">get(id, type)</code></dt>
   *   <dd class="param-desc">Replies an object given by its unique numeric id and a type.
   *   The type is either a string  "node", "way", or "relation", or one of
   *   the symbols
   *   {@class org.openstreetmap.josm.data.osm.OsmPrimitiveType}.NODE,
   *   {@class org.openstreetmap.josm.data.osm.OsmPrimitiveType}.WAY, or
   *   {@class org.openstreetmap.josm.data.osm.OsmPrimitiveType}.RELATION.</dd>
   *
   *   <dt><code class="signature">get(id)</code></dt>
   *   <dd class="param-desc">Replies an object given an ID. <code>id</code> is either an instance
   *   of
   *   {@class org.openstreetmap.josm.data.osm.PrimitiveId} or an object with
   *   the properties <code>id</code> and <code>type</code>, i.e.
   *   <code>{id: 1234, type: "node"}</code>.</dd>
   * </dl>
   *
   * @example
   * import { buildId , DataSetUtil, DataSet, OsmPrimitiveType} from 'josm/ds'
   *
   * const dsutil = new DataSetUtil(new DataSet())
   * // get a node
   * const n1  = dsutil.get(1234, 'node')
   *
   * // get a way
   * const w1 =  dsutil.get(3333, OsmPrimitiveType.WAY)
   *
   * // get a relation
   * const r1 = dsutil.get({id: 5423, type: 'relation'})
   *
   * // pass in a SimplePrimitiveId
   * const id = buildId(-5, OsmPrimitiveType.NODE)
   * const n2 = dsutil.get(id)
   *
   * // pass in a primitive to get it
   * const w2 = dsutil.wayBuilder().create(987)
   * const w3 = dsutil.get(w2)
   *
   * @param args see description
   */
  get () {
    const id = buildId(...arguments)
    return this.ds.getPrimitiveById(id)
  }

  /**
  * Replies the node with id <code>id</code>, or null.
  *
  * @example
  * import { DataSet, DataSetUtil } from 'josm/ds'
  *
  * const dsutil = new DataSetUtil(new DataSet())
  * // get a node
  * const n = dsutil.node(1234)
  *
  * @param {number} id  the unique numeric id. Must not be 0.
  * @returns {org.openstreetmap.josm.data.osm.Node} the node
  */
  node (id) {
    util.assert(util.isSomething(id), 'expected defined id, got "{0}"', id)
    return this.get(id, 'node')
  }

  /**
  * Replies the way with id <code>id</code>, or null
  *
  * @example
  * import { DataSet, DataSetUtil } from 'josm/ds'
  *
  * const dsutil = new DataSetUtil(new DataSet())
  * // get a way
  * const w  = dsutil.way(1234)
  * @param {number} id  the unique numeric id. Must not be 0.
  * @returns {org.openstreetmap.josm.data.osm.Way} the way
  */
  way (id) {
    util.assert(util.isSomething(id), 'expected defined id, got "{0}"', id)
    return this.get(id, 'way')
  }

  /**
  * Replies the relation with id <code>id</code>.
  *
  * @example
  * import { DataSet, DataSetUtil } from 'josm/ds'
  *
  * const dsutil = new DataSetUtil(new DataSet())
  * // get a relation
  * const r  = dsutil.relation(1234)
  *
  * @param {number} id  the unique numeric id. Must not be 0.
  * @returns {org.openstreetmap.josm.data.osm.Relation} the relation
  */
  relation (id) {
    util.assert(util.isSomething(id), 'expected defined id, got "{0}"', id)
    return this.get(id, 'relation')
  }

  /**
   * Run a sequence of operations against the dataset in "batch mode".
   *
   * Listeners to data set events are only notified at the end of the batch.
   *
   * @example
   * import { DataSet, DataSetUtil } from 'josm/ds'
   * const dsutil = new DataSetUtil(new DataSet())
   * // creates and adds two nodes and a way in batch operation
   * // to the dataset
   * dsutil.batch(() => {
   *    const n1 = dsutil.nodeBuilder().create()
   *    const n2 = dsutil.nodeBuilder().create()
   *    dsutil.wayBuilder().withNodes(n1,n2).create()
   * })
   *
   * @param {function} delegate  the function implementing the batch process.
   *     Ignored if null or undefined.
   */
  batch (delegate) {
    if (!(util.isSomething(delegate))) {
      return
    }
    util.assert(util.isFunction(delegate), 'expected a function, got "{0}"',
      delegate)
    this.ds.beginUpdate()
    try {
      delegate()
    } finally {
      this.ds.endUpdate()
    }
  }

  /**
   * Removes objects from the dataset
   *
   * <strong>Signatures</strong>
   * <dl>
   *   <dt><code class="signature">remove(id, type)</code></dt>
   *   <dd class="param-desc">Removes a single object given by its unique numeric ID (nid) and a
   *   type. The type is either a string  "node", "way", or "relation", or one
   *   of the symbols
   *   {@class org.openstreetmap.josm.data.osm.OsmPrimitiveType}.NODE,
   *   {@class org.openstreetmap.josm.data.osm.OsmPrimitiveType}.WAY, or
   *   {@class org.openstreetmap.josm.data.osm.OsmPrimitiveType}.RELATION.</dd>
   *
   *   <dt><code class="signature">remove(id, id, ...)</code></dt>
   *   <dd class="param-desc">Removes a collection of objects given by the ids. <code>id</code> is
   *   either an instance of
   *   {@class org.openstreetmap.josm.data.osm.PrimitiveId} or an object with
   *   the properties <code>id</code> and <code>type</code>, i.e.
   *   <code>{id: 1234, type: "node"}</code>.
   *   null and undefined are ignored.</dd>
   *
   *   <dt><code class="signature">remove(array|collection)</code></dt>
   *   <dd class="param-desc">Removes a collection of objects given by the an array or a
   *   java.util.Collection of ids.
   *   The collection elemeents are either instances of
   *   {@class org.openstreetmap.josm.data.osm.PrimitiveId} or an object with
   *   the properties <code>id</code> and <code>type</code>, i.e.
   *   <code>{id: 1234, type: "node"}</code>.
   *   null or undefined elements are ignored.
   *   </dd>
   * </dl>
   *
   * @example
   * import { DataSet, DataSetUtil, OsmPrimitiveType, buildId} from 'josm/ds'
   * const HashSet = Java.type('java.util.HashSet')
   * const dsutil = new DataSetUtil(new DataSet())
   *
   * // remove a node with a global id
   * dsutil.remove(1234, 'node')
   *
   * // remove a node and a way
   * const id1 = buildId(1234, 'node')
   * const id2 = buildId(3333, OsmPrimitiveType.WAY)
   * dsutil.remove(id1, id2)
   *
   * // remove a relation and a node
   * dsutil.remove({id: 1234, type: 'relation'}, id1)
   *
   * // remove an array of nodes
   * dsutil.remove([id1,id2])
   *
   * // remove a set of primitives
   * const ids = new HashSet()
   * ids.add(id1)
   * ids.add(id1)
   * dsutil.remove(ids)
   *
   * @param args see description
   */
  remove () {
    // we have exactly two arguments, id and type. If we succeed
    // to convert them to a primitive id, then we are done
    if (arguments.length === 2) {
      let id
      try {
        id = buildId(normalizeId(arguments[0]), normalizeType(arguments[1]))
      } catch (e) {
        id = null
      }
      if (id) {
        this.ds.removePrimitive(id)
        return
      }
    }

    // we have a list of ids or collections of ids to remove.
    // First build a flat list of the ids, then remove them
    // in a batch operation from the dataset
    const ids = normalizeIds(...arguments)
    const ds = this.ds
    this.batch(() => {
      each(ids, (id) => {
        ds.removePrimitive(id)
      })
    })
  }

  /**
   * Replies a node builder to create {@class org.openstreetmap.josm.data.osm.Node}s in this dataset.
   *
   * @example
   * import { DataSet, DataSetUtil } from 'josm/ds'
   * const dsutil = new DataSetUtil(new DataSet())
   * const n = dsutil.nodeBuilder
   *    .withId(1234,4567)
   *    .withTags({amenity: 'restaurant'})
   *    .create()
   * dsutil.has(n)
   *
   * @property {module:josm/builder~NodeBuilder} nodeBuilder
   * @readOnly
   */
  get nodeBuilder () {
    return NodeBuilder.forDataSet(this.ds)
  }

  /**
   * Replies a way builder to create ways in this dataset.
   *
   * @example
   * import { DataSet, DataSetUtil } from 'josm/ds'
   *
   * const dsutil = new DataSetUtil(new DataSet())
   * const nb = dsutil.nodeBuilder()
   * const w = dsutil.wayBuilder()
   *   .withNodes(nb.create(), nb.create())
   *   .create(1234, {tags: {highway: "residential"}})
   * dsutil.has(w)
   *
   * @property {module:josm/builder~WayBuilder} wayBuilder
   * @readOnly
   */
  get wayBuilder () {
    return WayBuilder.forDataSet(this.ds)
  }

  /**
   * Replies a relation builder to create relations in this dataset.
   *
   * @example
   * import { DataSet, DataSetUtil } from 'josm/ds'
   *
   * const dsutil = new DataSetUtil(new DataSet())
   * const r = dsutil.relationBuilder()
   *    .withId(8765,1234)
   *    .create({tags: {type: 'network'}})
   * ds.has(r)  // --> true
   *
   * @property  {module:josm/builder~RelationBuilder} relationBuilder
   * @readOnly
   */
  get relationBuilder () {
    return RelationBuilder.forDataSet(this.ds)
  }

  /**
   * Loads a dataset from a file.
   * <p>
   * Derives the format of the file from the file suffix, unless the named
   * option <code>options.format</code> is set.
   * <p>
   * <code>options</code> can contain the following named options:
   * <dl>
   *   <dt><code class="signature">format</code></dt>
   *   <dd class="param-desc">one of the strings <code>osm</code> (Open Street Map XML data),
   *   <code>osc</code> (Open Street Map change format), or
   *   <code>osm.gz</code> (Open Street Map XML data,
   *   compressed with gzip).  The format is normalized: white space is removed and it is
   *   converted to lower case.</dd>
   * </dl>
   *
   * @example
   * import { DataSetUtil } from 'josm/ds'
   *
   * // loads an OSM file
   * DataSetUtil.load('/path/to/my/file.osm')
   *
   * // loads an OSM file, explicity passing in the format
   * DataSetUtil.load('/path/to/my/file.any-suffix', { format 'osm' })
   *
   * @param {string|java.io.File}  source  the data source
   * @param {object} [options]  optional named parameters
   *
   * @return {module:josm/ds~DataSetUtil} the data set util with the loaded data set
   */
  static load (source, options) {
    function normalizeFile (source) {
      if (source instanceof File) {
        return source
      } else if (util.isString(source)) {
        return new File(source)
      } else {
        util.assert(false,
          'source: illegal value, expected string or File, got {0}',
          source)
      }
    }

    function normalizeFormat (source, options) {
      const FORMATS = {
        osm: true,
        osc: true,
        'osm.gz': true
      }
      if (util.isSomething(options.format)) {
        // convert to string
        const format = util.trim(options.format + '').toLowerCase()
        if (FORMATS[format]) {
          return format
        }
        util.assert(false,
          `options.format: unknown format '${format}'`)
      } else {
        if (source.getPath().endsWith('.osm.gz')) {
          return 'osm.gz'
        }
        if (new OsmImporter().acceptFile(source)) {
          return 'osm'
        }
        if (new OsmChangeImporter().acceptFile(source)) {
          return 'osc'
        }
        util.assert(false,
          `Failed to derive format from file name. file is '${source}'`)
      }
    }

    util.assert(util.isSomething(source),
      'source: must not be null or undefined')
    options = options || {}
    source = normalizeFile(source)
    const format = normalizeFormat(source, options)
    log(`format: ${format}`)
    let is
    try {
      switch (format) {
        // load an OSM file
        case 'osm': {
          is = new FileInputStream(source)
          const other = OsmReader.parseDataSet(is,
            null /* null progress monitor */)
          return new DataSetUtil(other)
        }

        // load an OSC file
        case 'osc': {
          is = new FileInputStream(source)
          const other = OsmChangeReader.parseDataSet(is,
            null /* null progress monitor */)
          return new DataSetUtil(other)
        }

        // load a compressed OSM file
        case 'osm.gz': {
          is = new GZIPInputStream(new FileInputStream(source))
          const other = OsmReader.parseDataSet(is,
            null /* null progress monitor */)
          return new DataSetUtil(other)
        }

        default:
          util.assert(false,
            `unknown format '${format}'. Failed to load from ${source}`)
      }
    } finally {
      is && Utils.close(is)
    }
  }

  /**
   * Saves the dataset to a file (in OSM XML format).
   * <p>
   *
   * <code>options</code> can contain the following named options:
   * <dl>
   *   <dt><code class="signature">version</code>: string</dt>
   *   <dd class="param-desc">the value of the attribute <code>version</code> in the OSM file
   *   header. Default: "0.6"</dd>
   *
   *   <dt><codeclass="signature">changeset</code>: Changeset</dt>
   *   <dd class="param-desc">the changeset whose id is included in the attribute
   *   <code>changeset</code> on every OSM object. If undefined, includes the
   *   individual <code>changeset</code> attribute of the OSM object.
   *   Default: undefined</dd>
   *   <dt><codeclass="signature">osmConform</code>: bool</dt>
   *   <dd class="param-desc">if true, prevents modification attributes to be written
   *   Default: true</dd>
   * </dl>
   *
   * @example
   * import { DataSetUtil } from 'josm/ds'
   *
   * const dsutil = new DataSetUtil()
   * // create a node in the dataset
   * dsutil.nodeBuilder()
   *  .withId(1, 1)
   *  .withPosition({ lat: 1.0, lon: 1.0 })
   *  .create()
   *
   * // save the dataset
   * dsutil.save('/tmp/my-dataset.osm')
   *
   * @param {string|java.io.File}  target  the target file
   * @param {object} [options] optional named parameters
   * @instance
   */
  save (target, options) {
    function normalizeTarget (target) {
      util.assert(util.isSomething(target),
        'target: must not be null or undefined')
      if (util.isString(target)) {
        return new File(target)
      } else if (target instanceof File) {
        return target
      } else {
        util.assert(false,
          'target: unexpected type of value, got {0}', target)
      }
    }

    function normalizeOptions (options) {
      options = options || {}
      util.assert(
        !util.isDef(options.version) || util.isString(options.version),
        'options.version: expected a string, got {0}', options.version)
      options.version = options.version
        ? util.trim(options.version)
        : null /* default version */

      /// true, if not explicity set to false
      options.osmConform = options.osmConform !== false

      const changeset = options.changeset
      util.assert(
        !util.isDef(changeset) || changeset instanceof Changeset,
        'options.changeset: expected a changeset, got {0}', changeset)
      return options
    }

    target = normalizeTarget(target)
    options = normalizeOptions(options)
    let pw
    try {
      pw = new PrintWriter(new FileWriter(target))
      const writer = OsmWriterFactory.createOsmWriter(
        pw,
        options.osmConform,
        options.version)
      if (options.changeset) {
        writer.setChangeset(options.changeset)
      }
      try {
        this.ds.getReadLock().lock()
        writer.header()
        writer.writeContent(this.ds)
        writer.footer()
      } finally {
        this.ds.getReadLock().unlock()
      }
    } finally {
      pw && pw.close()
    }
  }

  /**
   * Queries the dataset
   * <p>
   * <strong>Signatures</strong>
   * <dl>
   *   <dt><code class="signature">query(josmSearchExpression,?options)</code>
   *   </dt>
   *   <dd class="param-desc">Queries the dataset using the JOSM search expression
   *   <code>josmSearchExpression</code>.
   *   <code>josmSearchExpression</code> is a string as you would enter it in
   *   the JOSM search dialog. <code>options</code> is an (optional) object
   *   with named parameters, see below.</dd>
   *
   *   <dt><code class="signature">query(predicate,?options)</code></dt>
   *   <dd class="param-desc">Queries the dataset using a javascript predicate function
   *   <code>predicate</code>.  <code>predicate</code> is a javascript
   *   function which accepts a object as parameter and replies
   *   true, when it matches for the object ans false otherwise.
   *   <code>options</code> is an (optional) object with named parameters,
   *   see below.</dd>
   * </dl>
   *
   * The parameter <code>options</code> consist of the following (optional)
   * named parameters:
   * <dl>
   *   <dt><code class="signature">allElements</code> : boolean
   *   (Deprecated parameter names:
   *       <code class="signature">all</code>)</dt>
   *   <dd class="param-desc">If true, searches <em>all</em> objects in the dataset. If false,
   *   ignores incomplete or deleted
   *   objects. Default: false.</dd>
   *
   *   <dt><code class="signature">caseSensitive</code> : boolean</dt>
   *   <dd class="param-desc"><strong>Only applicable for searches with a JOSM search
   *   expression</strong>. If true,  searches case sensitive. If false,
   *   searches case insensitive. Default: false.</dd>
   *
   *   <dt><code class="signature">regexSearch</code> : boolean (Deprecated
   *       parameter names:
   *        <code class="signature">withRegexp</code>,
   *       <code class="signature">regexpSearch</code>)</dt>
   *   <dd class="param-desc"><strong>Only applicable for searches with a JOSM search
   *   expression</strong>. If true,  the search expression contains regular
   *   expressions. If false, it includes only plain strings for searching.
   *   Default: false.</dd>
   *
   *   <dt><code class="signature">mapCSSSearch</code></dt>
   *   <dd class="param-desc"><strong>Only applies for searches with a JOSM search
   *   expression</strong>.
   *    Default: false.</dd>
   * </dl>
   *
   * @example
   * import { DataSetUtil } from 'josm/ds'
   * const dsutil = new DataSetUtil()
   * // add or load primitives to query
   * // ...
   *
   * // query restaurants
   * const result1 = dsutil.query('amenity=restaurant')
   *
   * // query all nodes with a type query
   * const result2 = dsutil.query('type:node')
   *
   * // query using a custom predicate - all primitives
   * // with exactly two tags
   * const result3 = dsutil.query((primitive) => {
   *   primitive.getKeys().size() === 2
   * })
   *
   * @param {string|function} expression  the match expression
   * @param {object} [options] additional named parameters
   * @instance
   */
  query (expression, options) {
    const SearchSetting = Java.type('org.openstreetmap.josm.data.osm.search.SearchSetting')
    const SearchCompiler = Java.type('org.openstreetmap.josm.data.osm.search.SearchCompiler')
    options = options || {}

    switch (arguments.length) {
      case 0: return []
      case 1:
      case 2:
        if (util.isString(expression)) {
          const ss = new SearchSetting()
          ss.caseSensitive = Boolean(options.caseSensitive)
          ss.regexSearch =
            Boolean(options.regexSearch) ||
            Boolean(options.regexpSearch) ||
            Boolean(options.withRegexp)
          ss.allElements =
            Boolean(options.all) ||
            Boolean(options.allElements)

          ss.mapCSSSearch = Boolean(options.mapCSSSearch)
          ss.text = expression
          const matcher = SearchCompiler.compile(ss)
          let predicate
          if (ss.allElements) {
            predicate = (matcher) => (obj) => {
              return matcher.match(obj)
            }
          } else {
            predicate = (matcher) => (obj) => {
              return obj.isUsable() && matcher.match(obj)
            }
          }
          return collect(this.ds.allPrimitives(), predicate(matcher))
        } else if (util.isFunction(expression)) {
          const all =
            Boolean(options.all) ||
            Boolean(options.allElements)
          let predicate = expression
          if (!all) {
            predicate = (obj) => {
              return obj.isUsable() && expression(obj)
            }
          }
          return collect(this.ds.allPrimitives(), predicate)
        } else {
          util.assert(false,
            'expression: Unexpected type of argument, got {0}',
            arguments[0])
        }
        break
      default:
        util.assert(false,
          'Expected a predicate, got {0} arguments', arguments.length)
    }
  }
}