Source: josm/api.mjs

/**
 * This module provides functions to retrieve data from and upload data
 * to an OSM server.
 *
 * @example
*  import {Api, ChangesetApi, ApiConfig} from 'josm/api'
 *
 * @module josm/api
 */

/* global Java */

const URL = Java.type('java.net.URL')
const OsmApi = Java.type('org.openstreetmap.josm.io.OsmApi')
const Changeset = Java.type('org.openstreetmap.josm.data.osm.Changeset')
const OsmPrimitiveType = Java.type('org.openstreetmap.josm.data.osm.OsmPrimitiveType')
const PrimitiveId = Java.type('org.openstreetmap.josm.data.osm.PrimitiveId')
const SimplePrimitiveId = Java.type('org.openstreetmap.josm.data.osm.SimplePrimitiveId')
const NullProgressMonitor = Java.type('org.openstreetmap.josm.gui.progress.NullProgressMonitor')
const OsmServerChangesetReader = Java.type('org.openstreetmap.josm.io.OsmServerChangesetReader')
const OsmServerObjectReader = Java.type('org.openstreetmap.josm.io.OsmServerObjectReader')
const OsmServerBackreferenceReader = Java.type('org.openstreetmap.josm.io.OsmServerBackreferenceReader')
const Preferences = Java.type('org.openstreetmap.josm.data.Preferences')
const Bounds = Java.type('org.openstreetmap.josm.data.Bounds')
const LatLon = Java.type('org.openstreetmap.josm.data.coor.LatLon')
const BoundingBoxDownloader = Java.type('org.openstreetmap.josm.io.BoundingBoxDownloader')
const CredentialsManager = Java.type('org.openstreetmap.josm.io.auth.CredentialsManager')
const RequestorType = Java.type('java.net.Authenticator.RequestorType')
const PasswordAuthentication = Java.type('java.net.PasswordAuthentication')

import * as util from 'josm/util'

/**
 * Specification of position as lat/lon-pair.
 *
 * @typedef LatLonSpec
 * @property {number} lat  the latitude
 * @property {number} lon  the longitude
 */

/**
 * Creates a {@class org.openstreetmap.josm.data.coor.LatLon} from a
 * javascript object.
 *
 * @example
 * import { buildLatLon } from 'josm/api'
 * const pos = buildLatLon({lat: 1, lon: 2});
 *
 * @param {module:josm/api~LatLonSpec} obj  a specification of the position
 * @static
 * @returns {org.openstreetmap.josm.data.coor.LatLon}
 * @summary Create a {@class org.openstreetmap.josm.data.coor.LatLon}
 *      from a javascript object.
 */
export function buildLatLon(obj) {
  util.assert(util.isSomething(obj), 'obj: must not be null or undefined');
  util.assert(typeof obj === 'object',
      'obj: expected an object, got {0}', obj);
  util.assert(util.isNumber(obj.lat),
      'obj.lat: expected a number, got {0}', obj.lat);
  util.assert(util.isNumber(obj.lon),
      'obj.lon: expected a number, got {0}', obj.lon);
  util.assert(LatLon.isValidLat(obj.lat),
      'obj.lat: expected a valid lat in the range [-90,90], got {0}',
      obj.lat);
  util.assert(LatLon.isValidLon(obj.lon),
      'obj.lon: expected a valid lon in the range [-180,180], got {0}',
      obj.lon);
  return new LatLon(obj.lat, obj.lon);
}

/**
 * Specification of a bounds as JavaScript object.
 *
 * @typedef BoundsSpec1
 * @property {number} minlat
 * @property {number} minlon
 * @property {number} maxlat
 * @property {number} maxlon
 * @example
 * const bounds = {
 *    minlat: 46.9479186, minlon: 7.4619484,
 *    maxlat: 46.9497642, maxlon: 7.4660683
 * }
 */

/**
 * Specification of a bounds as JavaScript object.
 *
 * @typedef BoundsSpec2
 * @property {module:josm/api~LatLonSpec} min the upper left point
 * @property {module:josm/api~LatLonSpec} max the lower right point
 * @example
 * const bounds = {
 *    min: {lat: 46.9479186, lon: 7.4619484},
 *    max: {lat: 46.9497642, lon: 7.4660683}
 * }
 */

/**
 * Creates a {@class org.openstreetmap.josm.data.Bounds} instance from a javascript object.
 *
 * @example
 * import { buildBounds } from 'josm/api'
 * const bounds1 = buildBounds({
 *    minlat: 46.9479186, minlon: 7.4619484,
 *    maxlat: 46.9497642, maxlon: 7.4660683
 * })
 *
 * const bounds2 = buildBounds({
 *    min: {lat: 46.9479186, lon: 7.4619484},
 *    max: {lat: 46.9497642, lon: 7.4660683}
 * })
 *
 * @param {BoundsSpec1|BoundsSpec2} obj  a javascript object
 * @returns {org.openstreetmap.josm.data.Bounds} the bounds
 * @static
 */
export function buildBounds(obj) {
  util.assert(util.isSomething(obj), 'obj: must not be null or undefined')
  util.assert(typeof obj === 'object',
      'obj: expected an object, got {0}', obj)

  function normalizeLat(obj,name) {
      util.assert(util.isDef(obj[name]),
          '{0}: missing mandatory property', name)
      util.assert(util.isNumber(obj[name]),
          '{0}: expected a number, got {1}', name, obj[name])
      util.assert(LatLon.isValidLat(obj[name]),
          '{0}: expected a valid lat, got {1}', name, obj[name])
      return obj[name]
  }

  function normalizeLon(obj,name) {
      util.assert(util.isDef(obj[name]),
          '{0}: missing mandatory property', name)
      util.assert(util.isNumber(obj[name]),
          '{0}: expected a number, got {1}', name, obj[name])
      util.assert(LatLon.isValidLon(obj[name]),
          '{0}: expected a valid lon, got {1}', name, obj[name])
      return obj[name]
  }

  if (util.isDef(obj.minlat)) {
      const minlat = normalizeLat(obj, "minlat")
      const minlon = normalizeLon(obj, "minlon")
      const maxlat = normalizeLat(obj, "maxlat")
      const maxlon = normalizeLon(obj, "maxlon")
      return new Bounds(minlat, minlon, maxlat, maxlon)
  } else if (util.isDef(obj.min)) {
      const min = buildLatLon(obj.min)
      const max = buildLatLon(obj.max)
      return new Bounds(min,max)
  } else {
      util.assert(false,
          'obj: expected an object {min:.., max:..} or '
      + '{minlat:, maxlat:, minlon:, maxlon:}, got {0}', obj)
  }
}

/**
 * Provides methods to open, close, get, update, etc. changesets on the OSM
 * API server.
 *
 * <strong>Note:</strong> this class doesn't provide a constructor. Methods
 * and properties are <code>static</code>.
 *
 * @example
 * // load the changeset api
 * import { ChangesetApi } from 'josm/api'
 *
 * // create a new changeset on the server
 * const cs = ChangesetApi.open()
 *
 * @summary Provides methods to open, close, get, update, etc. changesets on the OSM
 * API server.
 *
 */
export class ChangesetApi {

  /**
   * Creates and opens a changeset
   *
   * @example
   * import { ChangesetApi } from 'josm/api'
   * const Changeset = Java.type('org.openstreetmap.josm.data.osm.Changeset')
   *
   * // open a new changeset with no tags
   * const cs1 = ChangesetApi.open()
   *
   * // open a new changeset with the tags given by the supplied changeset
   * const cs2 = new Changeset()
   * cs2.put('comment', 'a test comment')
   * cs2 = ChangesetApi.open(cs2)
   *
   * // open a new changeset with the tags given by the object
   * const cs3 = ChangesetApi.open({comment: 'a test comment'})
   *
   * @returns {org.openstreetmap.josm.data.osm.Changeset} the changeset
   * @param {org.openstreetmap.josm.data.osm.Changeset | object} [changeset] the changeset to open
   */
  static open() {
    let cs
    switch (arguments.length) {
      case 0:
        cs = new Changeset()
        break

      case 1:
        let o = arguments[0]
        if (o instanceof Changeset) {
          cs = o
        } else if (typeof o === 'object') {
          cs = new Changeset()
          for (let p in o) {
            if (!util.hasProp(o, p)) continue
            let key = p
            let value = o[p]
            key = util.trim(key)
            value = value + '' // convert to string
            cs.put(key, value)
          }
        } else {
          util.assert(false,
            'Unexpected type of argument, expected Changeset or object, ' +
            'got {0}', o)
        }
        break

      default:
        util.assert(false, 'Unexpected number of arguments, got {0}',
          arguments.length)
    }
    const api = OsmApi.getOsmApi()
    api.openChangeset(cs, NullProgressMonitor.INSTANCE)
    return cs
  }

  /**
   * Closes a changeset
   *
   * @example
   * import { ChangesetApi } from 'josm/api'
   * import * as util from 'josm/util'
   * const Changeset = Java.type('org.openstreetmap.josm.data.osm.Changeset')
   *
   * // closs the changeset 12345
   * ChangesetApi.close(12345)
   *
   * // open a new changeset with the tags given by the supplied changeset
   * const cs2 = new Changeset(12345)
   * cs2 = ChangesetApi.close(cs2)
   * util.assert(cs2.closed)  // the changeset is now closed
   *
   * @param {number | org.openstreetmap.josm.data.osm.Changeset} changeset the changeset to close
   * @returns {org.openstreetmap.josm.data.osm.Changeset} the changeset
   */
  static close() {
    let cs
    switch (arguments.length) {
      case 0:
        util.assert(false,
          'Missing arguments. Expected a changeset it or a changeset')
        break

      case 1: {
        const o = arguments[0]
        if (o instanceof Changeset) {
          cs = o
        } else if (util.isNumber(o)) {
          util.assert(o > 0, 'Expected a positive changeset id, got {0}', o)
          cs = new Changeset(o)
        } else {
          util.assert(false,
            'Unexpected type of argument, expected Changeset or number, ' +
            'got {0}', o)
        }
        break
      }

      default:
        util.assert(false, 'Unexpected number of arguments, got {0}',
          arguments.length)
    }
    const api = OsmApi.getOsmApi()
    api.closeChangeset(cs, NullProgressMonitor.INSTANCE)
    return cs
  }

  /**
   * Updates a changeset
   *
   * @example
   * import { ChangesetApi } from 'josm/api'
   * const Changeset = Java.type('org.openstreetmap.josm.data.osm.Changeset')
   *
   * // update the comment of a changeset
   * const cs2 = new Changeset(12345)
   * cs2.put('comment', 'an updated comment')
   * cs2 = ChangesetApi.update(cs2)
   *
   * @param {org.openstreetmap.josm.data.osm.Changeset} changeset  the changeset to update
   * @returns {org.openstreetmap.josm.data.osm.Changeset} the changeset
   */
  static update() {
    let cs
    switch (arguments.length) {
      case 0:
        util.assert(false, 'Missing arguments. Expected a changeset')
        break

      case 1: {
        const o = arguments[0]
        if (o instanceof Changeset) {
          cs = o
        } else {
          util.assert(false,
            'Unexpected type of argument, expected Changeset, got {0}', o)
        }
        break
      }

      default:
        util.assert(false, 'Unexpected number of arguments, got {0}',
          arguments.length)
    }
    const api = OsmApi.getOsmApi()
    api.updateChangeset(cs, NullProgressMonitor.INSTANCE)
    return cs
  }

  /**
   * Get a changeset from the server
   *
   * @example
   * import { ChangesetApi } from 'josm/api'
   * const Changeset = Java.type('org.openstreetmap.josm.data.osm.Changeset')
   *
   * // get the changeset with id 12345
   * const cs1 = ChangesetApi.get(12345)
   *
   * // get the changeset with id 12345
   * lets cs2 = new Changeset(12345)
   * cs2 = ChangesetApi.get(cs2)
   *
   * @param {number|org.openstreetmap.josm.data.osm.Changeset} changeset the changeset to get
   * @returns {org.openstreetmap.josm.data.osm.Changeset} the changeset
   */
  static get() {
    let cs
    switch (arguments.length) {
      case 0:
        util.assert(false,
          'Missing arguments. Expected a changeset id or a changeset')
        break

      case 1: {
        const o = arguments[0]
        if (o instanceof Changeset) {
          cs = o
        } else if (util.isNumber(o)) {
          util.assert(o > 0, 'Expected a positive changeset id, got {0}', o)
          cs = new Changeset(o)
        } else {
          util.assert(false,
            'Unexpected type of argument, expected Changeset or number, ' +
            'got {0}', o)
        }
        break
      }

      default:
        util.assert(false, 'Unexpected number of arguments, got {0}',
          arguments.length)
    }
    const reader = new OsmServerChangesetReader()
    cs = reader.readChangeset(cs.id, NullProgressMonitor.INSTANCE)
    return cs
  }
}

/**
 * Collection of static methods to download objects from and upload objects
 * to the OSM server.
 *
 * <strong>Note:</strong> this class doesn't provide a constructor.
 * Methods and properties are 'static'.
 *
 * @example
 * // load the api
 * import { Api } from 'josm/api'
 *
 * // download node 12345
 * const ds = Api.downloadObject(12345, 'node')
 *
 * @summary Collection of static methods to download objects from and upload objects
 *  to the OSM server
 */
export class Api {

  static #normalizeType (type) {
    util.assert(util.isSomething(type), 'type must not be null or undefined')
    if (util.isString(type)) {
      try {
        type = OsmPrimitiveType.fromApiTypeName(type)
      } catch (e) {
        util.assert(false, 'Invalid primitive type, got \'\'{0}\'\'', type)
      }
    } else if (type instanceof OsmPrimitiveType) {
      if (![OsmPrimitiveType.NODE, OsmPrimitiveType.WAY, OsmPrimitiveType.RELATION].includes(type)) {
        util.assert(false, 'Invalid primitive type, got {0}', type)
      }
    } else {
      util.assert(false, 'Invalid primitive type, got {0}', type)
    }
    return type
  }

  static #normalizeId (id) {
    util.assert(util.isSomething(id), 'id must not be null or nothing')
    util.assert(util.isNumber(id), 'Expected a number as id, got {0}', id)
    util.assert(id > 0, 'Expected a positive number as id, got {0}', id)
    return id
  }

  static #primitiveIdFromObject(o) {
    util.assert(util.hasProp(o, 'id'),
      'Mandatory property \'\'id\'\' is missing in object {0}', o)
    util.assert(util.hasProp(o, 'type'),
      'Mandatory property \'\'type\'\' is missing in object {0}', o)
    return new SimplePrimitiveId(Api.#normalizeId(o.id), Api.#normalizeType(o.type))
  }

  static #downloadObject1() {
    let id
    const o = arguments[0]
    util.assert(util.isSomething(o),
      'Argument 0: must not be null or undefined')
    if (o instanceof PrimitiveId) {
      id = o
    } else if (typeof o === 'object') {
      id = Api.#primitiveIdFromObject(o)
    } else {
      util.assert(false, 'Argument 0: unexpected type, got {0}', o)
    }
    var reader = new OsmServerObjectReader(id, false)
    var ds = reader.parseOsm(null /* null progress monitor */)
    return ds
  }

  static #optionFull(options) {
    if (!util.hasProp(options, 'full')) return undefined
    var o = options.full
    if (typeof o === 'boolean') return o
    util.assert('Expected a boolean value for option \'\'full\'\', got {0}', o)
  }

  static #optionVersion(options) {
    if (!util.hasProp(options, 'version')) return undefined
    var o = options.version
    util.assert(util.isNumber(o),
      'Expected a number for option \'\'version\'\', got {0}', o)
    util.assert(o > 0,
      'Expected a number > 0 for option \'\'version\'\', got {0}', o)
    return o
  }

  static #downloadObject2 () {
    function parseOptions (arg) {
      const options = { full: undefined, version: undefined }
      if (!(typeof arg === 'object')) {
        return options
      }
      options.full = Api.#optionFull(arg)
      options.version = Api.#optionVersion(arg)
      return options
    }

    let id
    let options = { full: undefined, version: undefined }

    if (util.isNumber(arguments[0])) {
      id = Api.#normalizeId(arguments[0])
      const type = Api.#normalizeType(arguments[1])
      id = new SimplePrimitiveId(id, type)
    } else if (arguments[0] instanceof PrimitiveId) {
      id = arguments[0]
      options = parseOptions(arguments[1])
    } else if (typeof arguments[0] === 'object') {
      id = Api.#primitiveIdFromObject(arguments[0])
      options = parseOptions(arguments[1])
    } else {
      util.assert(false, 'Unsupported types of arguments')
    }
    let reader
    if (util.isDef(options.version)) {
      reader = new OsmServerObjectReader(id, options.version)
    } else {
      reader = new OsmServerObjectReader(id, !!options.full)
    }
    const ds = reader.parseOsm(null /* null progress monitor */)
    return ds
  }

  static #downloadObject3 () {
    const options = { full: undefined, version: undefined }
    let n = Api.#normalizeId(arguments[0])
    let type = Api.#normalizeType(arguments[1])
    let id = new SimplePrimitiveId(n, type)

    util.assert(typeof arguments[2] === 'object',
      'Expected an object with named parameters, got {0}', arguments[2])
    options.full = Api.#optionFull(arguments[2])
    options.version = Api.#optionVersion(arguments[2])
    let reader
    if (util.isDef(options.version)) {
      reader = new OsmServerObjectReader(id, options.version)
    } else {
      reader = new OsmServerObjectReader(id, !!options.full)
    }
    var ds = reader.parseOsm(null /* null progress monitor */)
    return ds
  }

  /**
   * Options for the method downloadObject()
   *
   * @typedef DownloadObjectOptions
   * @property {boolean} [full=false] if <code>true</code>, the object and its immediate children are
   *   downloaded, i.e. the nodes of a way and the relation members of a relation. Default:
   *   <code>false</code>.
   * @property {number} [version] if present, the specified version of the object is downloaded.
   *   If missing, the current version is downloaded. If present, the
   *   option <code>full</code> is ignored.
   */

  /**
   * Downloads an object from the server.
   *
   * There are multiple options to specify what object to download.
   * In addition, the function accepts a set of optional named parameters
   * as last argument.
   *
   * <dl>
   *   <dt><code class='signature'>downloadObject(id, type, ?options)</code></dt>
   *   <dd class="param-desc"><code>id</code> is the global numeric id.
   *   <code>type</code> is either one of the strings 'node', 'way',
   *   or 'relation', or one of the  enumeration OsmPrimitiveType.NODE,
   *   OsmPrimitiveType.WAY, or OsmPrimitiveType.RELATION
   *   </dd>
   *
   *   <dt><code class='signature'>downloadObject(id, ?options)</code></dt>
   *   <dd class="param-desc"><code>id</code> is a <code>PrimitiveId</code> or an object
   *   with the (mandatory) properties <code>id</code> and <code>type</code>,
   *   i.e. an object <code>{id: ..., type: ...}</code>.
   *   <code>id</code> is again a number, <code>type</code> is again either one
   *   of the strings 'node', 'way', or 'relation', or one of the
   *   enumeration OsmPrimitiveType.NODE, OsmPrimitiveType.WAY,
   *   or OsmPrimitiveType.RELATION.
   *   </dd>
   * </dl>
   *
   * @example
   * import { Api } from 'josm/api'
   * const SimplePrimitiveId = Java.type('org.openstreetmap.josm.data.osm.SimplePrimitiveId')
   * const OsmPrimitiveType = Java.type('org.openstreetmap.josm.data.osm.OsmPrimitiveType')
   *
   * // download the node with id 12345
   * const ds1 = Api.downloadObject(12345, 'node')
   *
   * // download the node with id 12345
   * const ds2 = Api.downloadObject({id: 12345, type: 'node'})
   *
   * // download the full relation (including its members) with id 12345
   * const id = new SimplePrimitiveId(12345, OsmPrimitiveType.RELATION)
   * const ds3 = Api.downloadObject(id, {full: true})
   *
   * // download version 5 of the full way 12345 (including its nodes)
   * const ds4 = Api.downloadObject(12345, OsmPrimitiveType.WAY, {full: true, version: 5})
   *
   * @returns {org.openstreetmap.josm.data.osm.DataSet} the downloaded primitives
   * @param {number|org.openstreetmap.josm.data.osm.PrimitiveId} id the id of the object
   * @param {string|org.openstreetmap.josm.data.osm.OsmPrimitiveType} [type] the type of the object
   * @param {module:josm/api~DownloadObjectOptions} [options] named options
   * @static
   */
  static downloadObject() {
    switch (arguments.length) {
      case 0:
        util.assert(false, 'Unexpected number of arguments, got {0}',
          arguments.length)
        break
      case 1:
        return Api.#downloadObject1(...arguments)

      case 2:
        return Api.#downloadObject2(...arguments)

      case 3:
        return Api.#downloadObject3(...arguments)

      default:
        util.assert(false, 'Unexpected number of arguments, got {0}',
          arguments.length)
    }
  }

  static #downloadReferrer1 () {
    let id
    const o = arguments[0]
    util.assert(util.isSomething(o),
      'Argument 0: must not be null or undefined')
    if (o instanceof PrimitiveId) {
      id = o
    } else if (typeof o === 'object') {
      id = Api.#primitiveIdFromObject(o)
    } else {
      util.assert(false, 'Argument 0: unexpected type, got {0}', o)
    }
    const reader = new OsmServerBackreferenceReader(id.getUniqueId(),id.getType())
    const ds = reader.parseOsm(NullProgressMonitor.INSTANCE)
    return ds
  }

  static #downloadReferrer2 () {
    let id
    let type
    const options = { full: undefined }
    if (util.isNumber(arguments[0])) {
      id = Api.#normalizeId(arguments[0])
      type = Api.#normalizeType(arguments[1])
      id = new SimplePrimitiveId(id, type)
    } else if (arguments[0] instanceof PrimitiveId) {
      id = arguments[0]
      const o = arguments[1]
      if (util.isSomething(o)) {
        util.assert(typeof o === 'object',
          'Expected an object with named parameters, got {0}', o)
        options.full = Api.#optionFull(o)
      }
    } else if (typeof arguments[0] === 'object') {
      id = Api.#primitiveIdFromObject(arguments[0])
      const o = arguments[1]
      if (util.isSomething(o)) {
        util.assert(typeof o === 'object',
          'Expected an object with named parameters, got {0}', o)
        options.full = Api.#optionFull(o)
      }
    } else {
      util.assert(false, 'Unsupported types of arguments')
    }
    const reader = new OsmServerBackreferenceReader(id.getUniqueId(),
      id.getType())
    if (options.full) {
      reader.setReadFull(true)
    }
    const ds = reader.parseOsm(NullProgressMonitor.INSTANCE)
    return ds
  }

  static #downloadReferrer3 () {
    const options = { full: undefined }
    const n = Api.#normalizeId(arguments[0])
    const type = Api.#normalizeType(arguments[1])
    const id = new SimplePrimitiveId(n, type)

    util.assert(typeof arguments[2] === 'object',
      'Expected an object with named parameters, got {0}', arguments[2])
    options.full = Api.#optionFull(arguments[2])

    const reader = new OsmServerBackreferenceReader(id.getUniqueId(),
      id.getType())
    if (options.full) {
      reader.setReadFull(true)
    }
    const ds = reader.parseOsm(NullProgressMonitor.INSTANCE)
    return ds
  }

  /**
   * Options for the method donwloadReferrers()
   *
   * @typedef DownloadReferrerOptions
   * @property {boolean} [full=false] If <code>true</code>, the <strong>full</strong> objects are
   *   retrieved using multi-gets. If missing or <code>false</code>,
   *   only proxy objects are downloaded. Default: false
   */

  /**
   * Downloads the objects <em>referring</em> to another object from
   * the server.
   *
   * Downloads primitives from the OSM server which
   * refer to a specific primitive. Given a node, the referring ways and
   * relations are downloaded. Given a way or a relation, only referring
   * relations are downloaded.
   *
   * The default behaviour is to reply proxy objects only.
   *
   * If you set the option <code>{full: true}</code>, every referring object
   * is downloaded in full.
   *
   * There are multiple options to specify what referrers to download.
   * In addition, the function accepts a set of optional named parameters as
   * last argument.
   *
   * <dl>
   *   <dt><code class='signature'>downloadReferrer(id, type, ?options)
   *       </code></dt>
   *   <dd class="param-desc"><code>id</code> is the global numeric id.
   *   <code>type</code> is either one of the strings 'node', 'way', or
   *   'relation', or one of the  enumeration
   *   {@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'>downloadReferrer(id, ?options)</code></dt>
   *   <dd class="param-desc"><code>id</code> is a <code>PrimitiveId</code> or an object
   *   with the (mandatory) properties <code>id</code> and <code>type</code>,
   *   i.e. an object <code>{id: ..., type: ...}</code>.
   *   <code>id</code> is again a number, <code>type</code> is again either one
   *   of the strings 'node', 'way', or 'relation', or one of the
   *   enumeration
   *   {@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>
   * </dl>
   *
   * @example
   * import { Api } from 'josm/api'
   * import { NodeBuilder } from 'josm/builder'
   * const SimplePrimitiveId = Java.type('org.openstreetmap.josm.data.osm.SimplePrimitiveId')
   * const OsmPrimitiveType = Java.type('org.openstreetmap.josm.data.osm.OsmPrimitiveType')
   *
   * // download the objects referring to the node with id 12345
   * const ds1 = Api.downloadReferrer(12345, 'node')
   *
   * // download the objects referring to the node with id 12345
   * const ds2 = Api.downloadReferrer({id: 12345, type: 'node'})
   *
   * // download the relations referring to the  relation with id 12345.
   * // Referring relations are downloaded in full.
   * const id = new SimplePrimitiveId(12345, OsmPrimitiveType.RELATION)
   * const ds3 = Api.downloadReferrer(id, { full: true })
   *
   * // create the global node 12345 ...
   * const node = NodeBuilder.create(12345)
   * // ... and downloads its referrers in full
   * const ds = Api.downloadReferrer(node, { full: true })
   *
   * @returns {org.openstreetmap.josm.data.osm.DataSet} the downloaded primitives
   * @param {number|org.openstreetmap.josm.data.osm.PrimitiveId} id the id of the object
   * @param {string|org.openstreetmap.josm.data.osm.OsmPrimitiveType} [type] the type of the object
   * @param {module:josm/api~DownloadReferrerOptions} [options] named options
   */
  static downloadReferrer() {
    switch (arguments.length) {
      case 0:
        util.assert(false, 'Unexpected number of arguments, got {0}',
          arguments.length)
        break

      case 1:
        return Api.#downloadReferrer1(...arguments)

      case 2:
        return Api.#downloadReferrer2(...arguments)

      case 3:
        return Api.#downloadReferrer3(...arguments)

      default:
        util.assert(false, 'Unexpected number of arguments, got {0}',
          arguments.length)
    }
  }

  /**
   * Downloads the objects within a bounding box.
   *
   * @example
   * import { Api } from 'josm/api'
   * const Bounds = Java.type('org.openstreetmap.josm.data.Bounds')
   * const LatLon = Java.type('org.openstreetmap.josm.data.coor.LatLon')
   * const ds1 = Api.downloadArea(new Bounds(
   *     new LatLon(46.9479186,7.4619484),   // min
   *     new LatLon(46.9497642, 7.4660683)   // max
   * ))
   *
   * const ds2 = Api.downloadArea({
   *     min: {lat: 46.9479186, lon: 7.4619484},
   *     max: {lat: 46.9497642, lon: 7.4660683}
   * })
   *
   * @returns {org.openstreetmap.josm.data.osm.DataSet} the downloaded primitives
   * @param {org.openstreetmap.josm.data.Bounds|module:josm/api~BoundsSpec1|module:josm/api~BoundsSpec2} bounds the bounding box
   */
  static downloadArea() {

    util.assert(arguments.length === 1, 'Expected 1 argument, got {0}',
      arguments.length)
    let bounds = arguments[0]
    util.assert(util.isSomething(bounds),'bounds: must not be null or undefined')
    if (bounds instanceof Bounds) {
      // do nothing
    } else if (typeof bounds === 'object') {
      bounds = buildBounds(bounds) // convert to bounds
    } else {
      util.assert(false,
        'expected an instance of Bounds or an object, got {0}', bounds)
    }
    const downloader = new BoundingBoxDownloader(bounds)
    return downloader.parseOsm(NullProgressMonitor.INSTANCE)
  }

  /**
   * Options for the method upload()
   *
   * @typedef UploadOptions
   * @property {string|org.openstreetmap.josm.io.UploadStrategy}[strategy] Indicates how the data is uploaded.
   *    Either one of the strings
   *     <ul>
   *          <li>individualobjects</li>
   *          <li>chunked</li>
   *          <li>singlerequest</li>
   *       </ul>
   *    or one of the enumeration values in
   *    {@class org.openstreetmap.josm.io.UploadStrategy}.
   *    Default value: UploadStrategy.DEFAULT_UPLOAD_STRATEGY
   * @property {number| org.openstreetmap.josm.data.osm.Changeset} [changeset] The changeset to which the data is uploaded.
   *    Default: creates a new changeset
   *
   * @property {number} [chunkSize]  the size of an upload chunk  if the data is uploaded with the
   *    upload strategy {@class org.openstreetmap.josm.io.UploadStrategy}.CHUNKED_DATASET_STRATEGY.
   *
   * @property {boolean} [closeChangeset=true] if true, closes the changeset after the upload
   */

  /**
   * Uploads objects to the server.
   *
   * You can submit data either as
   * {@class org.openstreetmap.josm.data.osm.DataSet},
   * {@class org.openstreetmap.josm.data.APIDataSet}, javascript array of
   * {@class org.openstreetmap.josm.data.osm.OsmPrimitive}s or
   * a {@class java.util.Collection} of
   * {@class org.openstreetmap.josm.data.osm.OsmPrimitive}s.
   *
   * This method supports the same upload strategy as the JOSM upload dialog.
   * Supply the named parameter <code>{strategy: ...}</code> to choose the
   * strategy.
   *
   * <p class='documentation-warning'>
   * Be careful when uploading data to the OSM server! Do not upload copyright-
   * protected or test data.
   * </p>
   *
   *
   * The method takes care to update the primitives in the uploaded data when
   * the upload succeeds. For instance, uploaded new primitives become global
   * objects and get assigned their new id and version, successfully deleted
   * objects become invisible, etc.
   *
   * Even if the entire upload of a dataset fails, a subset therefore may
   * have been uploaded successfully. In order to keep track, which pritives
   * have been uploaded successfully in case of an error, the method replies a
   * collection of the successfully uploaded objects.
   *
   * @example
   * const DataSet = Java.type('org.openstreetmap.josm.data.osm.DataSet')
   * import { WayBuilder, NodeBuilder } from 'josm/builder'
   * import { Api } from 'josm/api'
   * const ds = new DataSet()
   * const nb = NodeBuilder.forDataSet(ds)
   * WayBuilder
   *  .forDataSet(ds)
   *  .withNodes(
   *     nb.withTags({name: 'node1'}).create(),
   *     nb.withTags({name: 'node2'}).create()
   *  )
   *  .withTags({name: 'way1'})
   *  .create()
   *
   * // uploads the data in a new changeset in one chunk
   * const processed = Api.upload(ds, 'just testing')
   *
   * @param {org.openstreetmap.josm.data.osm.DataSet|
   *         org.openstreetmap.josm.data.APIDataSet|array|java.util.Collection} data the data to upload
   * @param {string} comment the upload comment
   * @param {module:josm/api~UploadOptions} [options] named options
   * @returns {java.util.Collection}
   */
  static upload(data, comment, options) {
    const UploadStrategy = Java.type('org.openstreetmap.josm.io.UploadStrategy')
    const Changeset = Java.type('org.openstreetmap.josm.data.osm.Changeset')
    const APIDataSet = Java.type('org.openstreetmap.josm.data.APIDataSet')
    const DataSet = Java.type('org.openstreetmap.josm.data.osm.DataSet')
    const UploadStrategySpecification =
      Java.type('org.openstreetmap.josm.io.UploadStrategySpecification')
    const Collection = Java.type('java.util.Collection')
    const OsmServerWriter = Java.type('org.openstreetmap.josm.io.OsmServerWriter')

    comment = comment || ''
    comment = String(comment)

    util.assertSomething(data, 'data: must not be null or undefined')
    options = options || {}
    util.assert(typeof options === 'object',
      'options: expected an object with named arguments, got {0}', options)

    function normalizeChunkSize (size) {
      util.assert(util.isNumber(size),
        'chunksize: expected a number, got {0}', size)
      util.assert(size >= -1,
        'chunksize: expected -1 or a number > 0, got {0}', size)
      return size
    }

    function normalizeChangeset (changeset) {
      if (util.isNothing(changeset)) {
        return new Changeset()
      } else if (util.isNumber(changeset)) {
        util.assert(changeset > 0,
          'changeset: expected a changeset id > 0, got {0}', changeset)
        return new Changeset(changeset)
      } else if (changeset instanceof Changeset) {
        return changeset
      } else {
        util.assert(false, 'changeset: unexpected value, got {0}',
          changeset)
      }
    }

    function uploadSpecFromOptions (options) {
      let strategy = options.strategy || UploadStrategy.DEFAULT_UPLOAD_STRATEGY
      if (strategy instanceof String) {
        strategy = UploadStrategy.valueOf(strategy)
        util.assert(strategy, "invalid upload strategy ''{0}''", strategy)
      }
  
      let chunkSize = options.chunkSize ||
        UploadStrategySpecification.UNSPECIFIED_CHUNK_SIZE
      chunkSize = normalizeChunkSize(chunkSize)

      let closeChangeset = util.isDef(options.closeChangeset)
        ? options.closeChangeset 
        : true
      closeChangeset = Boolean(closeChangeset)

      const spec = new UploadStrategySpecification()
      spec.setStrategy(strategy)
      spec.setChunkSize(chunkSize)
      spec.setCloseChangesetAfterUpload(closeChangeset)
      return spec
    }

    let apiDataSet
    if (data instanceof DataSet) {
      apiDataSet = new APIDataSet(data)
    } else if (data instanceof APIDataSet) {
      apiDataSet = data
    } else if (util.isArray(data)) {
      apiDataSet = new APIDataSet(data)
    } else if (data instanceof Collection) {
      apiDataSet = new APIDataSet(data)
    } else {
      util.assert(false, 'data: unexpected type of value, got {0}', data)
    }

    if (apiDataSet.isEmpty()) return undefined
    apiDataSet.adjustRelationUploadOrder()
    const toUpload = apiDataSet.getPrimitives()

    let changeset = options.changeset || new Changeset()
    changeset = normalizeChangeset(changeset)
    changeset.put('comment', comment)
    const spec = uploadSpecFromOptions(options)
    const writer = new OsmServerWriter()

    writer.uploadOsm(spec, toUpload, changeset, null /* progress monitor */)
    if (spec.isCloseChangesetAfterUpload()) {
      ChangesetApi.close(changeset)
    }

    return writer.getProcessedPrimitives()
  }
}

/* -------------------------------------------------------------------------- */
/* ApiConfig                                                                  */
/* -------------------------------------------------------------------------- */
/**
 * ApiConfig provides methods and properties for configuring API parameters.
 *
 *
 * @class
 * @summary ApiConfig provides methods and properties for configuring API parameters
 * @name ApiConfig
 */
export const ApiConfig = {}

const DEFAULT_URL = 'https://www.openstreetmap.org/api'

/**
 * Get or set the API server URL.
 *
 * <dl>
 *   <dt><code class='signature'>get</code></dt>
 *   <dd class="param-desc">Replies the currently configured server URL or undefinend, if no
 *   server URL is configured.</dd>
 *
 *   <dt><code class='signature'>set</code></dt>
 *   <dd class="param-desc">Sets the current server URL. If null or undefined, removes the
 *   current configuration. Accepts either a string or a {@class java.net.URL}.
 *   Only accepts http or https URLs.
 *   </dd>
 * </dl>
 *
 * @example
 * import { ApiConfig } from 'josm/api'
 * ApiConfig.serverUrl   // -> the current server url
 *
 * // set a new API url
 * ApiConfig.serverUrl = 'http://api06.dev.openstreetmap.org'
 *
 * @static
 * @summary Get or set the API server URL.
 * @property {string} serverUrl
 * @name serverUrl
 * @memberof module:josm/api~ApiConfig
 */
Object.defineProperty(ApiConfig, 'serverUrl', {
  enumerable: true,
  get: function () {
    var url = Preferences.main().get('osm-server.url', null)
    if (url == null) url = DEFAULT_URL
    return url == null ? undefined : util.trim(url)
  },

  set: function (value) {
    if (util.isNothing(value)) {
      Preferences.main().put('osm-server.url', null)
    } else if (value instanceof URL) {
      util.assert(
        ['http', 'https'].includes(value.getProtocol()),
        'url: expected a http or https URL, got {0}',
        value)
      Preferences.main().put('osm-server.url', value.toString())
    } else if (util.isString(value)) {
      value = util.trim(value)
      try {
        const url = new URL(value)
        util.assert(
          ['http', 'https'].includes(url.getProtocol()),
          'url: expected a http or https URL, got {0}',
          url.toString())
        Preferences.main().put('osm-server.url', url.toString())
      } catch (e) {
        util.assert(false,
          'url: doesn\'\'t look like a valid URL, got {0}. Error: {1}',
          value, e)
      }
    } else {
      util.assert(false, 'Unexpected type of value, got {0}', value)
    }
  }
})

/**
 * Get the default server URL.
 *
 * @example
 * import { ApiConfig } from 'josm/api'
 * ApiConfig.defaultServerUrl   // -> the default server url
 *
 * @static
 * @summary Get the default server URL
 * @name defaultServerUrl
 * @property {string} defaultServerUrl the default server URL
 * @readOnly
 * @memberof module:josm/api~ApiConfig
 */
Object.defineProperty(ApiConfig, 'defaultServerUrl', {
  value: DEFAULT_URL,
  writable: false,
  enumerable: true
})

function normalizeAuthMethod (authMethod) {
  util.assert(util.isString(authMethod),
    'authMethod: expected a string, got {0}',
    authMethod)
  authMethod = util.trim(authMethod).toLowerCase()
  util.assert(authMethod === 'basic' || authMethod === 'oauth',
    'Unsupported value for authMethod, got {0}', authMethod)
  return authMethod
}

/**
 * Get or set the authentication method.
 *
 * JOSM uses two authentication methods:
 * <dl>
 *    <dt><code class='signature'>basic</code></dt>
 *    <dd class="param-desc">Basic authentication with a username and a password</dd>
 *    <dt><code class='signature'>oauth</code></dt>
 *    <dd class="param-desc">Authentication with the <a href='http://oauth.net/'>OAuth</a>
 *        protocol.</dd>
 * </dl>
 *
 * @example
 * import { ApiConfig } from 'josm/api'
 * ApiConfig.authMethod   // -> the current authentication method
 *
 * // set OAuth as authentication method
 * ApiConfig.authMethod = 'oauth'
 *
 * @static
 * @summary Get or set the authentication method.
 * @type string
 * @name authMethod
 * @property {string} authMethod the authentication method
 * @memberof module:josm/api~ApiConfig
 */
Object.defineProperty(ApiConfig, 'authMethod', {
  enumerate: true,
  get: function () {
    let authMethod = Preferences.main().get('osm-server.auth-method', 'basic')
    authMethod = util.trim(authMethod).toLowerCase()
    if (authMethod === 'basic' || authMethod === 'oauth') return authMethod
    // unsupported value for authMethod in the preferences. Returning
    // 'basic' as default.
    return 'basic'
  },
  set: function (value) {
    value = normalizeAuthMethod(value)
    Preferences.main().put('osm-server.auth-method', value)
  }
})

/**
 * Options for the method setCredentials
 *
 * @typedef SetOrGetCredentialOptions
 * @param {string} [host] the host name of the API server for which credentials are set.
 *    If missing, the host name of the currently configured OSM API server
 *    is used.
 */

/**
 * Basic credentials replied by getCredentials
 *
 * @typedef BasicCredentials
 * @property {string} host  the host name
 * @property {string} user  the user name
 * @property {string} password  the password
 */

/**
 * OAuth credentials replied by getCredentials
 *
 * @typedef OAuthCredentials
 * @property {string} key  the OAuth key
 * @property {string} secret  the OAuth secret
 */

/**
 * Gets the credentials, i.e. username and password for the basic
 * authentication method.
 *
 * @example
 * import { ApiConfig } from 'josm/api'
 *
 * // get username/password for the current OSM API server
 * const credentials = ApiConfig.getCredentials('basic')
 *
 * @param {string} authMethod  the authentication method. Either <code>basic</code> or <code>oauth</code>
 * @param {module:josm/api~SetOrGetCredentialOptions} [options] additional options
 * @static
 * @returns {BasicCredentials | OAuthCredentials}  the credentials
 * @memberof module:josm/api~ApiConfig
*/
function getCredentials(authMethod, options) {

  options = options || {}
  util.assert(typeof options === 'object',
    'options: expected an object with named options, got {0}', options)

  // a hack to convert a Java 'char[]' into a JavaScript string
  function charArrayToString(chars) {
    let result = ""
    for (let i=0; i < chars.length; i++) {
      const c = chars[i].toString()
      result += c
    }
    return result
  }

  function getBasicCredentials () {
    const cm = CredentialsManager.getInstance()
    if (options.host) options.host = util.trim(String(options.host))
    const host = options.host ? options.host : OsmApi.getOsmApi().getHost()
    const pa = cm.lookup(RequestorType.SERVER, host)
    if (pa) {
      return {
        host: host,
        user: pa.getUserName(),
        password: charArrayToString(pa.getPassword())
      }
    } else {
      return  {
        host: host,
        user: undefined,
        password: undefined
      }
    }
  }

  function getOAuthCredentials () {
    const cm = CredentialsManager.getInstance()
    const token = cm.lookupOAuthAccessToken()
    if (token == null) return undefined
    return { key: token.getKey(), secret: token.getSecret() }
  }

  authMethod = normalizeAuthMethod(authMethod)
  if (authMethod === 'basic') return getBasicCredentials()
  if (authMethod === 'oauth') return getOAuthCredentials()
  util.assert(false, 'Unsupported authentication method, got {0}',
    authMethod)
}
ApiConfig.getCredentials = getCredentials


function normalizeBasicCredentials (credentials) {

  if (util.isNothing(credentials)) return null
  util.assert(credentials instanceof PasswordAuthentication || typeof credentials === 'object',
    'basic credentials: expected an object or an instance of ' +
    'PasswordAuthentication , got {0}', credentials)

  if (credentials instanceof PasswordAuthentication) {
    return credentials
  } else {
    const user = String(credentials.user || '')
    let password = credentials.password || null
    if (password) {
      // convert to char array
      password = [...password]
    }
    return new PasswordAuthentication(user, password)
  }
}

function normalizeOAuthCredentials (credentials) {
  const OAuthToken = Java.type('org.openstreetmap.josm.data.oauth.OAuthToken')
  if (util.isNothing(credentials)) return null
  util.assert(credentials instanceof OAuthToken || typeof credentials === 'object',
    'oauth credentials: expected an object or an instance of OAuthToken, ' +
    'got {0}', credentials)
  if (credentials instanceof OAuthToken) {
    return credentials
  } else {
    const key = String(credentials.key || '')
    const secret = String(credentials.secret || '')
    return new OAuthToken(key, secret)
  }
}

/**
 * Userid and password for basic authentication.
 *
 * @typdef BasicAuthParameters
 * @param {string} user  the user id
 * @param {string} password the password
 */

/**
 * Parameters for OAuth authentication
 *
 * @typdef OAuthParameters
 * @param {string} key  the key
 * @param {string} secret the secret
 */

/**
 * Set the credentials, i.e. username and password for the basic
 * authentication method.
 *
 * Basic authentication credentials are either an instance of
 * {@class java.net.PasswordAuthentication} or
 * an object <code>{user: string, password: string}</code>.
 *
 * OAuth authentication credentials are either an instance of
 * {@class org.openstreetmap.josm.data.oauth.OAuthToken} or
 * an object <code>{key: string, secret: string}</code>.
 *
 * @example
 * import { ApiConfig } from 'josm/api'
 *
 * // set the credentials
 * ApiConfig.setCredentials('basic', { user:'test', password:'my-password' })
 *
 * @param {string} authMethod  the authentication method. Either 'basic' or 'oauth'.
 * @param {(
 *  module:josm/api~BasicAuthParameters
 * | module:josm/api~OAuthParameters
 * | org.openstreetmap.josm.data.oauth.OAuthToken
 * | java.net.PasswordAuthentication)} credentials  the credentials
 * @param {module:josm/api~SetOrGetCredentialOptions} [options] additional options
 * @static
 * @returns {object} the credentials
 * @memberof module:josm/api~ApiConfig
 */
function setCredentials (authMethod, credentials, options) {
  options = options || {}
  util.assert(typeof options === 'object',
    'options: expected an object with named options, got {0}', options)
  authMethod = normalizeAuthMethod(authMethod)
  if (authMethod === 'basic') {
    credentials = normalizeBasicCredentials(credentials)
    util.assert(credentials != null,
      'credentials: can\'\'t store null credentials')
    let host = options.host ? String(options.host) : null
    host = host || OsmApi.getOsmApi().getHost()
    const cm = CredentialsManager.getInstance()
    cm.store(RequestorType.SERVER, host, credentials)
  } else if (authMethod === 'oauth') {
      credentials = normalizeOAuthCredentials(credentials)
    util.assert(credentials != null,
      'credentials: can\'\'t store null credentials')
    const cm = CredentialsManager.getInstance()
    cm.storeOAuthAccessToken(credentials)
  } else {
    util.assert(false, 'Unsupported authentication method, got {0}',
      authMethod)
  }
}
ApiConfig.setCredentials = setCredentials