···11import { html, LitElement, type TemplateResult } from 'lit'
22import app from '../models/app.ts'
33import { setMapNav } from '../utils/nav.ts'
44+import { coordsFromDirectInput } from '../utils/geocode.ts'
4556interface NominatimResult {
67 place_id: number
···6263 }
63646465 #search = async (query: string) => {
6666+ const direct = coordsFromDirectInput(query)
6767+ if (direct) {
6868+ setMapNav({ ...direct, marker: true, name: query })
6969+ location.hash = '#!/'
7070+ return
7171+ }
7272+6573 this.loading = true
6674 this.error = null
6775 this.results = []
+154
www/utils/geocode.ts
···11+import OpenLocationCode from './open_location_code.ts'
22+import parseGeoUri from './parse_geouri.ts'
33+44+export type DirectCoords = { lat: number; lng: number; zoom: number }
55+66+export function coordsFromDirectInput(query: string): DirectCoords | null {
77+ return coordsFromGeoUri(query) ?? coordsFromPlusCode(query) ??
88+ coordsFromMapUrl(query)
99+}
1010+1111+/**
1212+ * Get coordinates from a google Use nominatim to get additional data about a location
1313+ * @reference https://developers.google.com/maps/documentation/urls/get-started
1414+ * @reference https://nominatim.org/release-docs/develop/api/Reverse/
1515+ *
1616+ * If it has lat lon in the url, use that:
1717+ * @example https://www.google.com/maps/place/Glorieta+de+los+Insurgentes/@19.4181529,-99.1652013,16z/data=!4m15!1m8!3m7!1s0x85d1ff39f2a2e4cd:0x2d9a33e867c32532!2sRoma+Nte.,+06700+Mexico+City,+CDMX!3b1!8m2!3d19.4195256!4d-99.162549!16s%2Fg%2F12hncjcx3!3m5!1s0x85d1ff36da5b09df:0x70f1041a2c6dd42e!8m2!3d19.4236852!4d-99.1629528!16s%2Fg%2F122152f5
1818+ *
1919+ * If it's a short-link, check if pluscode is in resp, and use that:
2020+ * @reference https://github.com/google/open-location-code/wiki/Supporting-plus-codes-in-your-app
2121+ * @example https://maps.app.goo.gl/x6uX5K2eXKDENxx56
2222+ *
2323+ * Known issues:
2424+ * - If a place isn't specific coordinates (an area), google doesn't assign pluscode
2525+ */
2626+2727+const baseNom = 'https://nominatim.openstreetmap.org/reverse'
2828+2929+export default async function getDataFromMapsUrl(url: string) {
3030+ let resp
3131+3232+ try {
3333+ resp = getCoordsFromGoogleMapsLongUrl(url)
3434+ } catch { /* didn't work */ }
3535+3636+ if (!resp?.lon || !resp?.lat) {
3737+ try {
3838+ resp = getCoordsFromOSMUrl(url)
3939+ } catch { /* didn't work */ }
4040+ }
4141+4242+ if (!resp?.lon || !resp?.lat) {
4343+ try {
4444+ resp = getCoordsFromAppleMapsUrl(url)
4545+ } catch { /* didn't work */ }
4646+ }
4747+4848+ if (resp?.lon && resp?.lat && !resp?.display_name?.length) {
4949+ const nomurl = `${baseNom}?lat=${resp.lat}&lon=${resp.lon}&format=json`
5050+ return await (await fetch(nomurl, {
5151+ headers: { 'Accept-Language': navigator.language || 'en' },
5252+ })).json()
5353+ }
5454+5555+ return resp
5656+}
5757+5858+const matchAppleCoords =
5959+ /(?:\?|&)ll=(-?\d{1,2}(?:\.\d+)?),(-?\d{1,3}(?:\.\d+)?)/
6060+const matchAppleAddress = /(?:\?|&)address=([^&]+)/
6161+6262+function getCoordsFromAppleMapsUrl(url: string) {
6363+ const [_coords, lat, lon] = url.match(matchAppleCoords) || [null, null, null]
6464+ const [_address, address] = url.match(matchAppleAddress) || [null, null]
6565+ if (lon && lat) {
6666+ return {
6767+ lon: parseFloat(lon),
6868+ lat: parseFloat(lat),
6969+ display_name: address?.length ? decodeAddress(address) : '',
7070+ }
7171+ }
7272+}
7373+7474+// Regex for matching lat lon from a url
7575+const matchGooglemapsCoords =
7676+ /@([-+]?\d{1,2}(?:\.\d+)?),([-+]?\d{1,3}(?:\.\d+)?)/
7777+const matchGoogleAddress = /(?:\?|&)address=([^&]+)/
7878+7979+function getCoordsFromGoogleMapsLongUrl(url: string) {
8080+ const [_, lat, lon] = url.match(matchGooglemapsCoords) || [null, null, null]
8181+ const [_address, address] = url.match(matchGoogleAddress) || [null, null]
8282+8383+ if (lon && lat) {
8484+ return {
8585+ lon: parseFloat(lon),
8686+ lat: parseFloat(lat),
8787+ display_name: address?.length ? decodeAddress(address) : '',
8888+ }
8989+ }
9090+}
9191+9292+function getCoordsFromOSMUrl(url: string) {
9393+ const matchLat = /mlat=([-+]?\d{1,2}(?:\.\d+)?)/
9494+ const matchLon = /mlon=([-+]?\d{1,3}(?:\.\d+)?)/
9595+9696+ const latMatch = url.match(matchLat)
9797+ const lonMatch = url.match(matchLon)
9898+9999+ if (latMatch && lonMatch) {
100100+ const lat = parseFloat(latMatch[1])
101101+ const lon = parseFloat(lonMatch[1])
102102+ return { lat, lon, display_name: '' }
103103+ } else {
104104+ return null
105105+ }
106106+}
107107+108108+function decodeAddress(encodedAddress = '') {
109109+ const decodedAddress = decodeURIComponent(encodedAddress)
110110+ const readableAddress = decodedAddress.replace(/\+/g, ' ')
111111+ return readableAddress
112112+}
113113+114114+function coordsFromGeoUri(query: string): DirectCoords | null {
115115+ const coords = parseGeoUri(query)
116116+ if (!coords) return null
117117+ let zoom = 15
118118+ if (coords.accuracy != null) {
119119+ if (coords.accuracy > 50000) zoom = 7
120120+ else if (coords.accuracy > 10000) zoom = 9
121121+ else if (coords.accuracy > 1000) zoom = 13
122122+ }
123123+ return { lat: coords.latitude, lng: coords.longitude, zoom }
124124+}
125125+126126+function coordsFromPlusCode(query: string): DirectCoords | null {
127127+ const trimmed = query.trim()
128128+ if (!OpenLocationCode.isFull(trimmed)) return null
129129+ const area = OpenLocationCode.decode(trimmed)
130130+ let zoom = 16
131131+ if (area.codeLength <= 4) zoom = 5
132132+ else if (area.codeLength <= 6) zoom = 9
133133+ else if (area.codeLength <= 8) zoom = 13
134134+ return { lat: area.latitudeCenter, lng: area.longitudeCenter, zoom }
135135+}
136136+137137+function coordsFromMapUrl(query: string): DirectCoords | null {
138138+ let result
139139+ try {
140140+ result = getCoordsFromGoogleMapsLongUrl(query)
141141+ } catch { /* */ }
142142+ if (!result?.lat || !result?.lon) {
143143+ try {
144144+ result = getCoordsFromOSMUrl(query)
145145+ } catch { /* */ }
146146+ }
147147+ if (!result?.lat || !result?.lon) {
148148+ try {
149149+ result = getCoordsFromAppleMapsUrl(query)
150150+ } catch { /* */ }
151151+ }
152152+ if (!result?.lat || !result?.lon) return null
153153+ return { lat: result.lat, lng: result.lon, zoom: 15 }
154154+}
+722
www/utils/open_location_code.ts
···11+/**
22+ * From: https://github.com/tspoke/typescript-open-location-code
33+ */
44+55+const LATITUDE_MAX = 90
66+const LONGITUDE_MAX = 180
77+88+/**
99+ * Coordinates of a decoded Open Location Code.
1010+ *
1111+ * The coordinates include the latitude and longitude of the lower left and
1212+ * upper right corners and the center of the bounding box for the area the
1313+ * code represents.
1414+ *
1515+ * @constructor
1616+ */
1717+export class CodeArea {
1818+ /**
1919+ * The latitude of the center in degrees.
2020+ */
2121+ public latitudeCenter: number
2222+ /**
2323+ * The longitude of the center in degrees.
2424+ */
2525+ public longitudeCenter: number
2626+2727+ constructor(
2828+ public latitudeLo: number,
2929+ public longitudeLo: number,
3030+ public latitudeHi: number,
3131+ public longitudeHi: number,
3232+ public codeLength: number,
3333+ ) {
3434+ this.latitudeCenter = Math.min(
3535+ latitudeLo + (latitudeHi - latitudeLo) / 2,
3636+ LATITUDE_MAX,
3737+ )
3838+ this.longitudeCenter = Math.min(
3939+ longitudeLo + (longitudeHi - longitudeLo) / 2,
4040+ LONGITUDE_MAX,
4141+ )
4242+ }
4343+4444+ public getLatitudeHeight(): number {
4545+ return this.latitudeHi - this.latitudeLo
4646+ }
4747+4848+ public getLongitudeWidth(): number {
4949+ return this.longitudeHi - this.longitudeLo
5050+ }
5151+}
5252+5353+/**
5454+ * Open Location Code implementation for TypeScript
5555+ */
5656+export default class OpenLocationCode {
5757+ public constructor(public code: string) {
5858+ }
5959+6060+ public getCode(): string {
6161+ return this.code
6262+ }
6363+6464+ /**
6565+ * Returns whether this {@link OpenLocationCode} is a padded Open Location Code, meaning that it
6666+ * contains less than 8 valid digits.
6767+ */
6868+ public isPadded(): boolean {
6969+ return this.code.indexOf(OpenLocationCode.PADDING_CHARACTER_) >= 0
7070+ }
7171+7272+ private static readonly CODE_PRECISION_NORMAL = 10
7373+ private static readonly CODE_PRECISION_EXTRA = 11
7474+ private static readonly MAX_DIGIT_COUNT_ = 15
7575+7676+ // A separator used to break the code into two parts to aid memorability.
7777+ private static readonly SEPARATOR_ = '+'
7878+7979+ // The number of characters to place before the separator.
8080+ private static readonly SEPARATOR_POSITION_ = 8
8181+8282+ // The character used to pad codes.
8383+ private static readonly PADDING_CHARACTER_ = '0'
8484+8585+ // The character set used to encode the values.
8686+ private static readonly CODE_ALPHABET_ = '23456789CFGHJMPQRVWX'
8787+8888+ // The base to use to convert numbers to/from.
8989+ private static readonly ENCODING_BASE_ =
9090+ OpenLocationCode.CODE_ALPHABET_.length
9191+9292+ // The maximum value for latitude in degrees.
9393+ static readonly LATITUDE_MAX_ = LATITUDE_MAX
9494+9595+ // The maximum value for longitude in degrees.
9696+ static readonly LONGITUDE_MAX_ = LONGITUDE_MAX
9797+9898+ // Maximum code length using lat/lng pair encoding. The area of such a
9999+ // code is approximately 13x13 meters (at the equator), and should be suitable
100100+ // for identifying buildings. This excludes prefix and separator characters.
101101+ private static readonly PAIR_CODE_LENGTH_ = 10
102102+103103+ // The resolution values in degrees for each position in the lat/lng pair
104104+ // encoding. These give the place value of each position, and therefore the
105105+ // dimensions of the resulting area.
106106+ private static readonly PAIR_RESOLUTIONS_ = [20.0, 1.0, .05, .0025, .000125]
107107+108108+ // First place value of the pairs (if the last pair value is 1).
109109+ private static readonly PAIR_FIRST_PLACE_VALUE_ = Math.pow(
110110+ OpenLocationCode.ENCODING_BASE_,
111111+ OpenLocationCode.PAIR_CODE_LENGTH_ / 2 - 1,
112112+ )
113113+114114+ // Inverse of the precision of the pair section of the code.
115115+ private static readonly PAIR_PRECISION_ = Math.pow(
116116+ OpenLocationCode.ENCODING_BASE_,
117117+ 3,
118118+ )
119119+120120+ // Number of digits in the grid precision part of the code.
121121+ private static readonly GRID_CODE_LENGTH_ =
122122+ OpenLocationCode.MAX_DIGIT_COUNT_ - OpenLocationCode.PAIR_CODE_LENGTH_
123123+124124+ // Number of columns in the grid refinement method.
125125+ private static readonly GRID_COLUMNS_ = 4
126126+127127+ // Number of rows in the grid refinement method.
128128+ private static readonly GRID_ROWS_ = 5
129129+130130+ // First place value of the latitude grid (if the last place is 1).
131131+ private static readonly GRID_LAT_FIRST_PLACE_VALUE_ = Math.pow(
132132+ OpenLocationCode.GRID_ROWS_,
133133+ OpenLocationCode.GRID_CODE_LENGTH_ - 1,
134134+ )
135135+136136+ // First place value of the longitude grid (if the last place is 1).
137137+ private static readonly GRID_LNG_FIRST_PLACE_VALUE_ = Math.pow(
138138+ OpenLocationCode.GRID_COLUMNS_,
139139+ OpenLocationCode.GRID_CODE_LENGTH_ - 1,
140140+ )
141141+142142+ // Multiply latitude by this much to make it a multiple of the finest
143143+ // precision.
144144+ private static readonly FINAL_LAT_PRECISION_ =
145145+ OpenLocationCode.PAIR_PRECISION_ *
146146+ Math.pow(
147147+ OpenLocationCode.GRID_ROWS_,
148148+ OpenLocationCode.MAX_DIGIT_COUNT_ - OpenLocationCode.PAIR_CODE_LENGTH_,
149149+ )
150150+151151+ // Multiply longitude by this much to make it a multiple of the finest
152152+ // precision.
153153+ private static readonly FINAL_LNG_PRECISION_ =
154154+ OpenLocationCode.PAIR_PRECISION_ *
155155+ Math.pow(
156156+ OpenLocationCode.GRID_COLUMNS_,
157157+ OpenLocationCode.MAX_DIGIT_COUNT_ - OpenLocationCode.PAIR_CODE_LENGTH_,
158158+ )
159159+160160+ // Minimum length of a code that can be shortened.
161161+ private static readonly MIN_TRIMMABLE_CODE_LEN_ = 6
162162+163163+ /**
164164+ * Determines if a code is valid.
165165+ *
166166+ * To be valid, all characters must be from the Open Location Code character
167167+ * set with at most one separator. The separator can be in any even-numbered
168168+ * position up to the eighth digit.
169169+ *
170170+ * @param {string} code The string to check.
171171+ * @return {boolean} True if the string is a valid code.
172172+ */
173173+ public static isValid(code: string): boolean {
174174+ if (!code) {
175175+ return false
176176+ }
177177+ // The separator is required.
178178+ if (code.indexOf(OpenLocationCode.SEPARATOR_) === -1) {
179179+ return false
180180+ }
181181+ if (
182182+ code.indexOf(OpenLocationCode.SEPARATOR_) !==
183183+ code.lastIndexOf(OpenLocationCode.SEPARATOR_)
184184+ ) {
185185+ return false
186186+ }
187187+ // Is it the only character?
188188+ if (code.length === 1) {
189189+ return false
190190+ }
191191+ // Is it in an illegal position?
192192+ if (
193193+ code.indexOf(OpenLocationCode.SEPARATOR_) >
194194+ OpenLocationCode.SEPARATOR_POSITION_ ||
195195+ code.indexOf(OpenLocationCode.SEPARATOR_) % 2 === 1
196196+ ) {
197197+ return false
198198+ }
199199+ // We can have an even number of padding characters before the separator,
200200+ // but then it must be the final character.
201201+ if (code.indexOf(OpenLocationCode.PADDING_CHARACTER_) > -1) {
202202+ // Short codes cannot have padding
203203+ if (
204204+ code.indexOf(OpenLocationCode.SEPARATOR_) <
205205+ OpenLocationCode.SEPARATOR_POSITION_
206206+ ) {
207207+ return false
208208+ }
209209+ // Not allowed to start with them!
210210+ if (code.indexOf(OpenLocationCode.PADDING_CHARACTER_) === 0) {
211211+ return false
212212+ }
213213+ // There can only be one group and it must have even length.
214214+ const padMatch = code.match(
215215+ new RegExp('(' + OpenLocationCode.PADDING_CHARACTER_ + '+)', 'g'),
216216+ )
217217+ if (
218218+ (padMatch?.length || 0) > 1 || (padMatch?.[0]?.length || 0) % 2 === 1 ||
219219+ (padMatch?.[0]?.length || 0) > OpenLocationCode.SEPARATOR_POSITION_ - 2
220220+ ) {
221221+ return false
222222+ }
223223+ // If the code is long enough to end with a separator, make sure it does.
224224+ if (code.charAt(code.length - 1) !== OpenLocationCode.SEPARATOR_) {
225225+ return false
226226+ }
227227+ }
228228+ // If there are characters after the separator, make sure there isn't just
229229+ // one of them (not legal).
230230+ if (code.length - code.indexOf(OpenLocationCode.SEPARATOR_) - 1 === 1) {
231231+ return false
232232+ }
233233+234234+ // Strip the separator and any padding characters.
235235+ const strippedCode = code.replace(
236236+ new RegExp('\\' + OpenLocationCode.SEPARATOR_ + '+'),
237237+ '',
238238+ )
239239+ .replace(new RegExp(OpenLocationCode.PADDING_CHARACTER_ + '+'), '')
240240+ // Check the code contains only valid characters.
241241+ for (let i = 0, len = strippedCode.length; i < len; i++) {
242242+ const character = strippedCode.charAt(i).toUpperCase()
243243+ if (
244244+ character !== OpenLocationCode.SEPARATOR_ &&
245245+ OpenLocationCode.CODE_ALPHABET_.indexOf(character) === -1
246246+ ) {
247247+ return false
248248+ }
249249+ }
250250+ return true
251251+ }
252252+253253+ /**
254254+ * Returns whether the provided Open Location Code is a padded Open Location Code, meaning that it
255255+ * contains less than 8 valid digits.
256256+ */
257257+ public static isPadded(code: string): boolean {
258258+ return new OpenLocationCode(code).isPadded()
259259+ }
260260+261261+ /**
262262+ * Determines if a code is a valid short code.
263263+ *
264264+ * @param {string} code The string to check.
265265+ * @return {boolean} True if the string can be produced by removing four or
266266+ * more characters from the start of a valid code.
267267+ */
268268+ public static isShort(code: string): boolean {
269269+ if (!OpenLocationCode.isValid(code)) {
270270+ return false
271271+ }
272272+ // If there are less characters than expected before the SEPARATOR.
273273+ return code.indexOf(OpenLocationCode.SEPARATOR_) >= 0 &&
274274+ code.indexOf(OpenLocationCode.SEPARATOR_) <
275275+ OpenLocationCode.SEPARATOR_POSITION_
276276+ }
277277+278278+ /**
279279+ * Determines if a code is a valid full Open Location Code.
280280+ *
281281+ * @param {string} code The string to check.
282282+ * @return {boolean} True if the code represents a valid latitude and longitude combination.
283283+ */
284284+ public static isFull(code: string): boolean {
285285+ if (!OpenLocationCode.isValid(code)) {
286286+ return false
287287+ }
288288+ // If it's short, it's not full.
289289+ if (OpenLocationCode.isShort(code)) {
290290+ return false
291291+ }
292292+293293+ // Work out what the first latitude character indicates for latitude.
294294+ const firstLatValue =
295295+ OpenLocationCode.CODE_ALPHABET_.indexOf(code.charAt(0).toUpperCase()) *
296296+ OpenLocationCode.ENCODING_BASE_
297297+ if (firstLatValue >= OpenLocationCode.LATITUDE_MAX_ * 2) {
298298+ return false // The code would decode to a latitude of >= 90 degrees.
299299+ }
300300+ if (code.length > 1) {
301301+ // Work out what the first longitude character indicates for longitude.
302302+ const firstLngValue =
303303+ OpenLocationCode.CODE_ALPHABET_.indexOf(code.charAt(1).toUpperCase()) *
304304+ OpenLocationCode.ENCODING_BASE_
305305+ if (firstLngValue >= OpenLocationCode.LONGITUDE_MAX_ * 2) {
306306+ return false // The code would decode to a longitude of >= 180 degrees.
307307+ }
308308+ }
309309+ return true
310310+ }
311311+312312+ public contains(latitude: number, longitude: number): boolean {
313313+ const codeArea = OpenLocationCode.decode(this.getCode())
314314+ return codeArea.latitudeLo <= latitude &&
315315+ latitude < codeArea.latitudeHi &&
316316+ codeArea.longitudeLo <= longitude &&
317317+ longitude < codeArea.longitudeHi
318318+ }
319319+320320+ /**
321321+ * Encode a location into an Open Location Code.
322322+ *
323323+ * @param {number} latitude The latitude in signed decimal degrees. It will
324324+ * be clipped to the range -90 to 90.
325325+ * @param {number} longitude The longitude in signed decimal degrees. Will be
326326+ * normalised to the range -180 to 180.
327327+ * @param {?number} codeLength The length of the code to generate. If
328328+ * omitted, the value OpenLocationCode.CODE_PRECISION_NORMAL will be used.
329329+ * For a more precise result, OpenLocationCode.CODE_PRECISION_EXTRA is
330330+ * recommended.
331331+ * @return {string} The code.
332332+ * @throws {Exception} if any of the input values are not numbers.
333333+ */
334334+ public static encode(
335335+ latitude: number,
336336+ longitude: number,
337337+ codeLength: number = OpenLocationCode.CODE_PRECISION_NORMAL,
338338+ ): string {
339339+ if (
340340+ codeLength < 2 ||
341341+ (codeLength < OpenLocationCode.PAIR_CODE_LENGTH_ && codeLength % 2 === 1)
342342+ ) {
343343+ throw new Error(
344344+ 'IllegalArgumentException: Invalid Open Location Code length',
345345+ )
346346+ }
347347+348348+ const editedCodeLength = Math.min(
349349+ OpenLocationCode.MAX_DIGIT_COUNT_,
350350+ codeLength,
351351+ )
352352+353353+ // Ensure that latitude and longitude are valid.
354354+ let editedLatitude = OpenLocationCode.clipLatitude(latitude)
355355+ const editedLongitude = OpenLocationCode.normalizeLongitude(longitude)
356356+ // Latitude 90 needs to be adjusted to be just less, so the returned code
357357+ // can also be decoded.
358358+ if (editedLatitude === 90) {
359359+ editedLatitude = editedLatitude -
360360+ OpenLocationCode.computeLatitudePrecision(editedCodeLength)
361361+ }
362362+ let code = ''
363363+364364+ // Compute the code.
365365+ // This approach converts each value to an integer after multiplying it by
366366+ // the final precision. This allows us to use only integer operations, so
367367+ // avoiding any accumulation of floating point representation errors.
368368+369369+ // Multiply values by their precision and convert to positive.
370370+ // Force to integers so the division operations will have integer results.
371371+ // Note: JavaScript requires rounding before truncating to ensure precision!
372372+ let latVal = Math.floor(
373373+ Math.round(
374374+ (editedLatitude + OpenLocationCode.LATITUDE_MAX_) *
375375+ OpenLocationCode.FINAL_LAT_PRECISION_ * 1e6,
376376+ ) / 1e6,
377377+ )
378378+ let lngVal = Math.floor(
379379+ Math.round(
380380+ (editedLongitude + OpenLocationCode.LONGITUDE_MAX_) *
381381+ OpenLocationCode.FINAL_LNG_PRECISION_ * 1e6,
382382+ ) / 1e6,
383383+ )
384384+385385+ // Compute the grid part of the code if necessary.
386386+ if (editedCodeLength > OpenLocationCode.PAIR_CODE_LENGTH_) {
387387+ for (
388388+ let i = 0;
389389+ i <
390390+ OpenLocationCode.MAX_DIGIT_COUNT_ -
391391+ OpenLocationCode.PAIR_CODE_LENGTH_;
392392+ i++
393393+ ) {
394394+ const latDigit = latVal % OpenLocationCode.GRID_ROWS_
395395+ const lngDigit = lngVal % OpenLocationCode.GRID_COLUMNS_
396396+ const ndx = latDigit * OpenLocationCode.GRID_COLUMNS_ + lngDigit
397397+ code = OpenLocationCode.CODE_ALPHABET_.charAt(ndx) + code
398398+ // Note! Integer division.
399399+ latVal = Math.floor(latVal / OpenLocationCode.GRID_ROWS_)
400400+ lngVal = Math.floor(lngVal / OpenLocationCode.GRID_COLUMNS_)
401401+ }
402402+ } else {
403403+ latVal = Math.floor(
404404+ latVal /
405405+ Math.pow(
406406+ OpenLocationCode.GRID_ROWS_,
407407+ OpenLocationCode.GRID_CODE_LENGTH_,
408408+ ),
409409+ )
410410+ lngVal = Math.floor(
411411+ lngVal /
412412+ Math.pow(
413413+ OpenLocationCode.GRID_COLUMNS_,
414414+ OpenLocationCode.GRID_CODE_LENGTH_,
415415+ ),
416416+ )
417417+ }
418418+ // Compute the pair section of the code.
419419+ for (let i = 0; i < OpenLocationCode.PAIR_CODE_LENGTH_ / 2; i++) {
420420+ code = OpenLocationCode.CODE_ALPHABET_.charAt(
421421+ lngVal % OpenLocationCode.ENCODING_BASE_,
422422+ ) + code
423423+ code = OpenLocationCode.CODE_ALPHABET_.charAt(
424424+ latVal % OpenLocationCode.ENCODING_BASE_,
425425+ ) + code
426426+ latVal = Math.floor(latVal / OpenLocationCode.ENCODING_BASE_)
427427+ lngVal = Math.floor(lngVal / OpenLocationCode.ENCODING_BASE_)
428428+ }
429429+430430+ // Add the separator character.
431431+ code = code.substring(0, OpenLocationCode.SEPARATOR_POSITION_) +
432432+ OpenLocationCode.SEPARATOR_ +
433433+ code.substring(OpenLocationCode.SEPARATOR_POSITION_)
434434+435435+ // If we don't need to pad the code, return the requested section.
436436+ if (editedCodeLength >= OpenLocationCode.SEPARATOR_POSITION_) {
437437+ return code.substring(0, editedCodeLength + 1)
438438+ }
439439+ // Pad and return the code.
440440+ return code.substring(0, editedCodeLength) +
441441+ Array(OpenLocationCode.SEPARATOR_POSITION_ - editedCodeLength + 1).join(
442442+ OpenLocationCode.PADDING_CHARACTER_,
443443+ ) + OpenLocationCode.SEPARATOR_
444444+ }
445445+446446+ /**
447447+ * Decodes an Open Location Code into its location coordinates.
448448+ *
449449+ * Returns a CodeArea object that includes the coordinates of the bounding
450450+ * box - the lower left, center and upper right.
451451+ *
452452+ * @param {string} code The code to decode.
453453+ * @return {CodeArea} An object with the coordinates of the
454454+ * area of the code.
455455+ * @throws {Exception} If the code is not valid.
456456+ */
457457+ public static decode(code: string): CodeArea {
458458+ // This calculates the values for the pair and grid section separately, using
459459+ // integer arithmetic. Only at the final step are they converted to floating
460460+ // point and combined.
461461+ if (!OpenLocationCode.isFull(code)) {
462462+ throw new Error(
463463+ 'IllegalArgumentException: Passed Open Location Code is not a valid full code: ' +
464464+ code,
465465+ )
466466+ }
467467+ // Strip the '+' and '0' characters from the code and convert to upper case.
468468+ const editedCode = code.replace(OpenLocationCode.SEPARATOR_, '')
469469+ .replace(
470470+ new RegExp(
471471+ OpenLocationCode.PADDING_CHARACTER_ + OpenLocationCode.SEPARATOR_,
472472+ ),
473473+ '',
474474+ )
475475+ .toUpperCase()
476476+477477+ // Initialise the values for each section. We work them out as integers and
478478+ // convert them to floats at the end.
479479+ let normalLat = -OpenLocationCode.LATITUDE_MAX_ *
480480+ OpenLocationCode.PAIR_PRECISION_
481481+ let normalLng = -OpenLocationCode.LONGITUDE_MAX_ *
482482+ OpenLocationCode.PAIR_PRECISION_
483483+ let gridLat = 0
484484+ let gridLng = 0
485485+ // How many digits do we have to process?
486486+ let digits = Math.min(editedCode.length, OpenLocationCode.PAIR_CODE_LENGTH_)
487487+ // Define the place value for the most significant pair.
488488+ let pv = OpenLocationCode.PAIR_FIRST_PLACE_VALUE_
489489+ // Decode the paired digits.
490490+ for (let position = 0; position < digits; position += 2) {
491491+ normalLat +=
492492+ OpenLocationCode.CODE_ALPHABET_.indexOf(editedCode.charAt(position)) *
493493+ pv
494494+ normalLng += OpenLocationCode.CODE_ALPHABET_.indexOf(
495495+ editedCode.charAt(position + 1),
496496+ ) * pv
497497+ if (position < digits - 2) {
498498+ pv /= OpenLocationCode.ENCODING_BASE_
499499+ }
500500+ }
501501+ // Convert the place value to a float in degrees.
502502+ let latPrecision = pv / OpenLocationCode.PAIR_PRECISION_
503503+ let lngPrecision = pv / OpenLocationCode.PAIR_PRECISION_
504504+ // Process any extra precision digits.
505505+ if (editedCode.length > OpenLocationCode.PAIR_CODE_LENGTH_) {
506506+ // Initialise the place values for the grid.
507507+ let rowpv = OpenLocationCode.GRID_LAT_FIRST_PLACE_VALUE_
508508+ let colpv = OpenLocationCode.GRID_LNG_FIRST_PLACE_VALUE_
509509+ // How many digits do we have to process?
510510+ digits = Math.min(editedCode.length, OpenLocationCode.MAX_DIGIT_COUNT_)
511511+ for (let i = OpenLocationCode.PAIR_CODE_LENGTH_; i < digits; i++) {
512512+ const digitVal = OpenLocationCode.CODE_ALPHABET_.indexOf(
513513+ editedCode.charAt(i),
514514+ )
515515+ const row = Math.floor(digitVal / OpenLocationCode.GRID_COLUMNS_)
516516+ const col = digitVal % OpenLocationCode.GRID_COLUMNS_
517517+ gridLat += row * rowpv
518518+ gridLng += col * colpv
519519+ if (i < digits - 1) {
520520+ rowpv /= OpenLocationCode.GRID_ROWS_
521521+ colpv /= OpenLocationCode.GRID_COLUMNS_
522522+ }
523523+ }
524524+ // Adjust the precisions from the integer values to degrees.
525525+ latPrecision = rowpv / OpenLocationCode.FINAL_LAT_PRECISION_
526526+ lngPrecision = colpv / OpenLocationCode.FINAL_LNG_PRECISION_
527527+ }
528528+ // Merge the values from the normal and extra precision parts of the code.
529529+ const lat = normalLat / OpenLocationCode.PAIR_PRECISION_ +
530530+ gridLat / OpenLocationCode.FINAL_LAT_PRECISION_
531531+ const lng = normalLng / OpenLocationCode.PAIR_PRECISION_ +
532532+ gridLng / OpenLocationCode.FINAL_LNG_PRECISION_
533533+ // Multiple values by 1e14, round and then divide. This reduces errors due
534534+ // to floating point precision.
535535+ return new CodeArea(
536536+ Math.round(lat * 1e14) / 1e14,
537537+ Math.round(lng * 1e14) / 1e14,
538538+ Math.round((lat + latPrecision) * 1e14) / 1e14,
539539+ Math.round((lng + lngPrecision) * 1e14) / 1e14,
540540+ Math.min(editedCode.length, OpenLocationCode.MAX_DIGIT_COUNT_),
541541+ )
542542+ }
543543+544544+ /**
545545+ * Recover the nearest matching code to a specified location.
546546+ *
547547+ * Given a valid short Open Location Code this recovers the nearest matching
548548+ * full code to the specified location.
549549+ *
550550+ * @param {string} shortCode A valid short code.
551551+ * @param {number} latitude The latitude to use for the reference
552552+ * location.
553553+ * @param {number} longitude The longitude to use for the reference
554554+ * location.
555555+ * @return {string} The nearest matching full code to the reference location.
556556+ * @throws {Exception} if the short code is not valid, or the reference
557557+ * position values are not numbers.
558558+ */
559559+ public static recoverNearest(
560560+ shortCode: string,
561561+ latitude: number,
562562+ longitude: number,
563563+ ): string {
564564+ if (!OpenLocationCode.isShort(shortCode)) {
565565+ if (OpenLocationCode.isFull(shortCode)) {
566566+ return shortCode
567567+ } else {
568568+ throw new Error(
569569+ 'ValueError: Passed short code is not valid: ' + shortCode,
570570+ )
571571+ }
572572+ }
573573+574574+ const referenceLatitude = OpenLocationCode.clipLatitude(latitude)
575575+ const referenceLongitude = OpenLocationCode.normalizeLongitude(longitude)
576576+ const shortCodeUpper = shortCode.toUpperCase() // Clean up the passed code.
577577+ // Compute the number of digits we need to recover.
578578+ const paddingLength = OpenLocationCode.SEPARATOR_POSITION_ -
579579+ shortCodeUpper.indexOf(OpenLocationCode.SEPARATOR_)
580580+ const resolution = Math.pow(20, 2 - (paddingLength / 2)) // The resolution (height and width) of the padded area in degrees.
581581+ const halfResolution = resolution / 2.0 // Distance from the center to an edge (in degrees).
582582+583583+ // Use the reference location to pad the supplied short code and decode it.
584584+ const codeArea = OpenLocationCode.decode(
585585+ OpenLocationCode.encode(referenceLatitude, referenceLongitude).substr(
586586+ 0,
587587+ paddingLength,
588588+ ) + shortCodeUpper,
589589+ )
590590+ // How many degrees latitude is the code from the reference? If it is more
591591+ // than half the resolution, we need to move it north or south but keep it
592592+ // within -90 to 90 degrees.
593593+ if (
594594+ referenceLatitude + halfResolution < codeArea.latitudeCenter &&
595595+ codeArea.latitudeCenter - resolution >= -OpenLocationCode.LATITUDE_MAX_
596596+ ) {
597597+ // If the proposed code is more than half a cell north of the reference location,
598598+ // it's too far, and the best match will be one cell south.
599599+ codeArea.latitudeCenter -= resolution
600600+ } else if (
601601+ referenceLatitude - halfResolution > codeArea.latitudeCenter &&
602602+ codeArea.latitudeCenter + resolution <= OpenLocationCode.LATITUDE_MAX_
603603+ ) {
604604+ // If the proposed code is more than half a cell south of the reference location,
605605+ // it's too far, and the best match will be one cell north.
606606+ codeArea.latitudeCenter += resolution
607607+ }
608608+609609+ // How many degrees longitude is the code from the reference?
610610+ if (referenceLongitude + halfResolution < codeArea.longitudeCenter) {
611611+ codeArea.longitudeCenter -= resolution
612612+ } else if (referenceLongitude - halfResolution > codeArea.longitudeCenter) {
613613+ codeArea.longitudeCenter += resolution
614614+ }
615615+616616+ return OpenLocationCode.encode(
617617+ codeArea.latitudeCenter,
618618+ codeArea.longitudeCenter,
619619+ codeArea.codeLength,
620620+ )
621621+ }
622622+623623+ /**
624624+ * Remove characters from the start of an OLC code.
625625+ *
626626+ * This uses a reference location to determine how many initial characters
627627+ * can be removed from the OLC code. The number of characters that can be
628628+ * removed depends on the distance between the code center and the reference
629629+ * location.
630630+ *
631631+ * @param {string} code The full code to shorten.
632632+ * @param {number} latitude The latitude to use for the reference location.
633633+ * @param {number} longitude The longitude to use for the reference location.
634634+ * @return {string} The code, shortened as much as possible that it is still
635635+ * the closest matching code to the reference location.
636636+ * @throws {Exception} if the passed code is not a valid full code or the
637637+ * reference location values are not numbers.
638638+ */
639639+ public static shorten(
640640+ code: string,
641641+ latitude: number,
642642+ longitude: number,
643643+ ): string {
644644+ if (!OpenLocationCode.isFull(code)) {
645645+ throw new Error('ValueError: Passed code is not valid and full: ' + code)
646646+ }
647647+ if (code.indexOf(OpenLocationCode.PADDING_CHARACTER_) !== -1) {
648648+ throw new Error('ValueError: Cannot shorten padded codes: ' + code)
649649+ }
650650+651651+ const codeUpper = code.toUpperCase()
652652+ const codeArea = OpenLocationCode.decode(codeUpper)
653653+ if (codeArea.codeLength < OpenLocationCode.MIN_TRIMMABLE_CODE_LEN_) {
654654+ throw new Error(
655655+ 'ValueError: Code length must be at least ' +
656656+ OpenLocationCode.MIN_TRIMMABLE_CODE_LEN_,
657657+ )
658658+ }
659659+660660+ const latitudeClipped = OpenLocationCode.clipLatitude(latitude)
661661+ const longitudeClipped = OpenLocationCode.normalizeLongitude(longitude)
662662+663663+ // How close are the latitude and longitude to the code center.
664664+ const range = Math.max(
665665+ Math.abs(codeArea.latitudeCenter - latitudeClipped),
666666+ Math.abs(codeArea.longitudeCenter - longitudeClipped),
667667+ )
668668+ for (let i = OpenLocationCode.PAIR_RESOLUTIONS_.length - 2; i >= 1; i--) {
669669+ // Check if we're close enough to shorten. The range must be less than 1/2
670670+ // the resolution to shorten at all, and we want to allow some safety, so
671671+ // use 0.3 instead of 0.5 as a multiplier.
672672+ if (range < (OpenLocationCode.PAIR_RESOLUTIONS_[i] * 0.3)) {
673673+ // Trim it.
674674+ return codeUpper.substring((i + 1) * 2)
675675+ }
676676+ }
677677+ return codeUpper
678678+ }
679679+680680+ /**
681681+ * Clip a latitude into the range -90 to 90.
682682+ *
683683+ * @param {number} latitude
684684+ * @return {number} The latitude value clipped to be in the range.
685685+ */
686686+ private static clipLatitude(latitude: number): number {
687687+ return Math.min(90, Math.max(-90, latitude))
688688+ }
689689+690690+ /**
691691+ * Compute the latitude precision value for a given code length.
692692+ * Lengths <= 10 have the same precision for latitude and longitude, but
693693+ * lengths > 10 have different precisions due to the grid method having
694694+ * fewer columns than rows.
695695+ * @param {number} codeLength
696696+ * @return {number} The latitude precision in degrees.
697697+ */
698698+ private static computeLatitudePrecision(codeLength: number): number {
699699+ if (codeLength <= 10) {
700700+ return Math.pow(20, Math.floor(codeLength / -2 + 2))
701701+ }
702702+ return Math.pow(20, -3) /
703703+ Math.pow(OpenLocationCode.GRID_ROWS_, codeLength - 10)
704704+ }
705705+706706+ /**
707707+ * Normalize a longitude into the range -180 to 180, not including 180.
708708+ *
709709+ * @param {number} longitude
710710+ * @return {number} Normalized into the range -180 to 180.
711711+ */
712712+ private static normalizeLongitude(longitude: number): number {
713713+ let longitudeOutput = longitude
714714+ while (longitudeOutput < -180) {
715715+ longitudeOutput = longitudeOutput + 360
716716+ }
717717+ while (longitudeOutput >= 180) {
718718+ longitudeOutput = longitudeOutput - 360
719719+ }
720720+ return longitudeOutput
721721+ }
722722+}
+56
www/utils/parse_geouri.ts
···11+/*
22+Copyright 2022 The Matrix.org Foundation C.I.C.
33+44+Licensed under the Apache License, Version 2.0 (the "License");
55+you may not use this file except in compliance with the License.
66+You may obtain a copy of the License at
77+88+ http://www.apache.org/licenses/LICENSE-2.0
99+1010+Unless required by applicable law or agreed to in writing, software
1111+distributed under the License is distributed on an "AS IS" BASIS,
1212+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313+See the License for the specific language governing permissions and
1414+limitations under the License.
1515+*/
1616+export default (
1717+ uri: string,
1818+): GeolocationCoordinates | undefined => {
1919+ function parse(s: string): number | null {
2020+ const ret = parseFloat(s)
2121+ if (Number.isNaN(ret)) {
2222+ return null
2323+ } else {
2424+ return ret
2525+ }
2626+ }
2727+2828+ const m = uri.match(/^\s*geo:(.*?)\s*$/)
2929+ if (!m) return
3030+ const parts = m[1].split(';')
3131+ const coords = parts[0].split(',')
3232+ let uncertainty: number | null | undefined = undefined
3333+ for (const param of parts.slice(1)) {
3434+ const m = param.match(/u=(.*)/)
3535+ if (m) uncertainty = parse(m[1])
3636+ }
3737+ const latitude = parse(coords[0])
3838+ const longitude = parse(coords[1])
3939+4040+ if (latitude === null || longitude === null) {
4141+ return
4242+ }
4343+4444+ return {
4545+ latitude: latitude!,
4646+ longitude: longitude!,
4747+ altitude: parse(coords[2]),
4848+ accuracy: uncertainty!,
4949+ altitudeAccuracy: null,
5050+ heading: null,
5151+ speed: null,
5252+ toJSON() {
5353+ return JSON.stringify(this)
5454+ },
5555+ }
5656+}