wgs84-geo-url.js

import { GeoParams, setGeoParamsBeforeSetHook } from "./geo-params.js"
import { GeoURL } from "./geo-url.js"

/**
 * URL interface for WGS84 geo URI with latitude, longitude and possibly altitude
 * @extends GeoURL
 */
export class WGS84GeoURL extends GeoURL {
	/**
	 * Create a WGS84GeoURL from a string
	 *
	 * Limited to WGS84 URIs, which lets interpret them as containing latitude/longitude/altitude.
	 * But that's almost all of the geo URIs because no other CRS is currently supported by RFC 5870.
	 * @param {string|URL|GeoURL} url - geo URI or relative reference
	 * @param {string|URL|GeoURL} [base] - base geo URI
	 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/URL/URL|MDN} for URL constructor
	 */
	constructor(url, base) {
		super(url, base)
		if (this.crs != "wgs84") {
			throw new TypeError(`Unexpected CRS ${this.crs}`)
		}
	}

	/**
	 * Create a WGS84GeoURL or return null on error
	 * @function parse
	 * @memberof WGS84GeoURL
	 * @param {string|URL|GeoURL} url - geo URI or relative reference
	 * @param {string|URL|GeoURL} [base] - base geo URI
	 * @returns {WGS84GeoURL|null}
	 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/URL/parse_static|MDN} for parse() static method
	 */

	/**
	 * Check if url is parsable as a valid WGS84 geo URI
	 * @function canParse
	 * @memberof WGS84GeoURL
	 * @param {string|URL|GeoURL} url - geo URI or relative reference
	 * @param {string|URL|GeoURL} [base] - base geo URI
	 * @returns {boolean}
	 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/URL/canParse_static|MDN} for canParse() static method
	 */

	/**
	 * geo URI parameters object
	 *
	 * The returned object will throw a TypeError if the "crs" parameter is updated to anything other than "wgs84", case-insensitively.
	 * @readonly
	 * @type {GeoParams}
	 */
	get geoParams() {
		const params = super.geoParams
		setGeoParamsBeforeSetHook(params, (name, value) => {
			if (name.toLowerCase() == "crs" && value.toLowerCase() != "wgs84") {
				throw new TypeError(`geoParams setter: ${value} is not a valid value for crs of WGS84GeoURL`)
			}
		})
		return params
	}

	/**
	 * Coordinates as a string of two or three comma-separated numbers, as they appear in the URL.
	 * @type {string}
	 * @example
	 * const url = new WGS84GeoURL("geo:60,30;u=10")
	 * console.log(url.coordinatesString) // outputs "60,30"
	 */
	/**
	 * Set coordinates to a string
	 * @method
	 * @instance
	 * @memberof WGS84GeoURL
	 * @name set coordinatesString
	 * @param {string} value - a string of two or three comma-separated numbers, as they appear in the URL
	 * @throws {TypeError} if coordinates are out of allowed range or if GeoURL's setter throws
	 * @example
	 * const url = new WGS84GeoURL("geo:60,30;u=10")
	 * url.coordinatesString = "61,31,5"
	 * console.log(url.toString()) // outputs "geo:61,31,5;u=10"
	 * url.coordinatesString = "0,181" // throws TypeError
	 * @see {@link WGS84GeoURL#coordinatesString} for the corresponding getter
	 */

	/**
	 * Coordinates array
	 * @type {number[]}
	 * @example
	 * const url = new WGS84GeoURL("geo:60,30;u=10")
	 * console.log(url.coordinates) // outputs "[ 60, 30 ]"
	 */
	/**
	 * Set coordinates to an array of numbers
	 * @method
	 * @instance
	 * @memberof WGS84GeoURL
	 * @name set coordinates
	 * @param {number[]} value - an array with two or three numbers
	 * @throws {TypeError} if coordinates are out of allowed range or if GeoURL's setter throws
	 * @example
	 * const url = new WGS84GeoURL("geo:60,30;u=10")
	 * url.coordinates = [61, 31, 5]
	 * console.log(url.toString()) // outputs "geo:61,31,5;u=10"
	 * @see {@link WGS84GeoURL#coordinates} for the corresponding getter
	 */

	/**
	 * Latitude-longitude pair
	 * @type {number[]}
	 */
	get latLon() {
		return [this.lat, this.lon]
	}
	/**
	 * Latitude-longitude pair
	 *
	 * An alternative name for {@link WGS84GeoURL#latLon}, favored by Leaflet
	 * @type {number[]}
	 * @example
	 * const url = new WGS84GeoURL("geo:13.4125,103.8667")
	 * L.marker(url.latLng)
	 */
	get latLng() {
		return this.latLon
	}
	/**
	 * Longitude-latitude pair
	 *
	 * The order is swapped as compared with the URI and {@link WGS84GeoURL#latLon}
	 * @type {number[]}
	 */
	get lonLat() {
		return [this.lon, this.lat]
	}
	/**
	 * Longitude-latitude pair
	 *
	 * An alternative name for {@link WGS84GeoURL#lonLat}
	 * @type {number[]}
	 */
	get lngLat() {
		return this.lonLat
	}

	/**
	 * Latitude in decimal degrees between -90 and 90
	 * @type {number}
	 * @see {@link https://datatracker.ietf.org/doc/html/rfc5870#section-3.4.2|RFC 5870} for component description
	 * @example
	 * const url = new WGS84GeoURL("geo:13.4125,103.8667")
	 * console.log(url.lat) // outputs "13.4125"
	 */
	get lat() {
		return this.coordA
	}
	/**
	 * Set the latitude
	 * @method
	 * @instance
	 * @memberof WGS84GeoURL
	 * @name set lat
	 * @param {number} value - latitude in decimal degrees between -90 and 90
	 * @throws {TypeError} if the value is out of range
	 * @example
	 * const url = new WGS84GeoURL("geo:13.4125,103.8667")
	 * url.lat = 16.4321
	 * console.log(url.toString()) // outputs "geo:16.4321,103.8667"
	 * @see {@link WGS84GeoURL#lat} for the corresponding getter
	 */
	set lat(value) {
		this.coordA = value
	}

	/**
	 * Latitude in decimal degrees between -90 and 90
	 *
	 * A longer name for {@link WGS84GeoURL#lat}
	 * @type {number}
	 */
	get latitude() {
		return this.coordA
	}
	set latitude(value) {
		this.coordA = value
	}

	/**
	 * Longitude in decimal degrees between -180 and 180
	 * @type {number}
	 * @see {@link https://datatracker.ietf.org/doc/html/rfc5870#section-3.4.2|RFC 5870} for component description
	 * @example
	 * const url = new WGS84GeoURL("geo:13.4125,103.8667")
	 * console.log(url.lon) // outputs "103.8667"
	 */
	get lon() {
		return this.coordB
	}
	/**
	 * Set the longitude
	 * @method
	 * @instance
	 * @memberof WGS84GeoURL
	 * @name set lon
	 * @param {number} value - longitude in decimal degrees between -180 and 180
	 * @throws {TypeError} if the value is out of range
	 * @example
	 * const url = new WGS84GeoURL("geo:13.4125,103.8667")
	 * url.lon = 101.1001
	 * console.log(url.toString()) // outputs "geo:13.4125,101.1001"
	 * @see {@link WGS84GeoURL#lon} for the corresponding getter
	 */
	set lon(value) {
		this.coordB = value
	}

	/**
	 * Longitude in decimal degrees between -180 and 180
	 *
	 * An alternative name for {@link WGS84GeoURL#lon}, as used for example in Leaflet
	 * @type {number}
	 */
	get lng() {
		return this.coordB
	}
	set lng(value) {
		this.coordB = value
	}

	/**
	 * Longitude in decimal degrees between -180 and 180
	 *
	 * A longer name for {@link WGS84GeoURL#lon}
	 * @type {number}
	 */
	get longitude() {
		return this.coordB
	}
	set longitude(value) {
		this.coordB = value
	}

	/**
	 * Altitude in meters
	 * @type {number|undefined}
	 * @see {@link https://datatracker.ietf.org/doc/html/rfc5870#section-3.4.2|RFC 5870} for component description
	 * @example
	 * const url = new WGS84GeoURL("geo:13.4125,103.8667")
	 * console.log(url.alt) // outputs "undefined"
	 * @example
	 * const url = new WGS84GeoURL("geo:48.201,16.3695,183")
	 * console.log(url.alt) // outputs "183"
	 */
	get alt() {
		return this.coordC
	}
	/**
	 * Set the altitude
	 * @method
	 * @instance
	 * @memberof WGS84GeoURL
	 * @name set alt
	 * @param {number|undefined} value - altitude in meters or undefined
	 * @throws {TypeError} if the value is neither a finite number nor undefined
	 * @example
	 * const url = new WGS84GeoURL("geo:13.4125,103.8667")
	 * url.alt = 65
	 * console.log(url.toString()) // outputs "geo:13.4125,103.8667,65"
	 * @example
	 * const url = new WGS84GeoURL("geo:48.201,16.3695,183")
	 * url.alt = undefined
	 * console.log(url.toString()) // outputs "geo:48.201,16.3695"
	 * @see {@link WGS84GeoURL#alt} for the corresponding getter
	 */
	set alt(value) {
		this.coordC = value
	}

	/**
	 * Altitude in meters
	 *
	 * A longer name for {@link WGS84GeoURL#alt}
	 * @type {number|undefined}
	 */
	get altitude() {
		return this.coordC
	}
	set altitude(value) {
		this.coordC = value
	}

	_validateCoordinates(coordinates) {
		super._validateCoordinates(coordinates)
		const [lat, lon] = coordinates
		if (lat < -90 || lat > 90) {
			throw new TypeError(`Latitude ${lat} outside of the allowed range`)
		}
		if (lon < -180 || lon > 180) {
			throw new TypeError(`Longitude ${lon} outside of the allowed range`)
		}
	}
}