Skip to content
Snippets Groups Projects
Commit 21a40826 authored by David Byers's avatar David Byers
Browse files

Initial commit.

parents
Branches
Tags
No related merge requests found
*~
\#*
.#*
.elc
This diff is collapsed.
README.md 0 → 100644
# Open Location Code for Emacs
Open Location Code is a way to encode locations in a format that is
easier for people (not computers) to use than latitude and longitude.
For example, the code 9FCQ9HXG+4C refers to the location 58°23'52.1"N
15°34'33.8"E (58.397813, 15.576063).
Codes can be shortened by removing characters from the beginning
andding a reference location: 9HXG+4C with the reference "Linköping"
would refer to the same set of coordinates.
For details about open location code and implementations in other
languages, see https://github.com/google/open-location-code.
## Description
This package implements an open location code library for emacs. It
implements all the required and most of the optional features in the
standard, and passes the test cases published in the open location
code github repository (see above).
### Data structures
#### OLC Area
An OLC is the area represented by an open location code. All fields
are read-only once the object has been created.
`(olc-area-create :latlo LATLO :lonlo LONLO :lathi LATHI :lonhi LONHI)`
: Creates an OLC area with southwest corner `LATLO`,`LONLO` and
northeast corner `LATHI`,`LONHI`.
`(olc-area-p OBJ)`
: Return non-nil if `OBJ` is an OLC area.
`(olc-area-latlo AREA)`
`(olc-area-lonlo AREA)`
`(olc-area-lathi AREA)`
`(olc-area-lonhi AREA)`
: Get the south, west, north, and east coordinates of the area,
respectively.
`(olc-area-lat AREA)`
`(olc-area-lon AREA)`
: Get the center latitude and longitude of the area, respectively.
#### OLC Parse
The OLC parse is a structure mainly used internally. Unless you call
`olc-parse-code` you will probably never see one.
`(olc-parse-create &keys pairs grid short prec code)`
: Creates an OLC parse structure. Don't call this: use
`olc-parse-code` instead.
`(olc-parse-pairs PARSE)`
: Returns the list of parsed pairs from the code (pairs are before the
plus sign and the first two characters after, if present).
`(olc-parse-grid PARSE)`
: Returns the list of parsed grid digits from the code (the optional
digits that follow the last pair).
`(olc-parse-short PARSE)`
: Non-nil if the parsed code was shortened.
`(olc-parse-precision PARSE)`
: Precision of the parsed code. Padded codes can have precisions lower
than 8. All other full and all short codes have precision of at
least 8 (although, don't cound on short codes always having
precision 8 or more).
`(olc-parse-code PARSE)`
: The parsed code.
### Functions
`(olc-encode lat lon len)`
: Encode a latitude LAT, longitude LON, into an open location code of
length LEN. All arguments will be clipped to acceptable values.
`(olc-decode code)`
: Decode a code CODE. Returns an OLC area (see above).
`(olc-recover code lat lon &optional format)`
: Recover the closest point to coordinates `LAT` and `LON` with a code
that can be shortened to `CODE`. If FORMAT is `'latlon`, then the
center of the recovered area `(LAT . LON)` is returned. If FORMAT is
`'area` (or any other value), the returned value is an full open
location code.
`(olc-recover-string arg1 &optional arg2 arg3)`
: Recover a shortened code *without* the reference latitude and
longitude. When called with one argument, it must be a string
consisting of a shortened open location code followed by whitespace
and a geographical location. When called with two strings, the first
must be a shortened open location code and the second if the
geographical location. Optionally, the last argument in either case
can be a symbol indicating the format of the return value (see
`olc-recover`, above). This function requires the `request` package
to be installed, and uses the Open Streetmap API to convert the
geographical reference to coordinates. Please make sure you follow
the acceptable use policy for the API (e.g., one request per second,
tops, allowed).
olc.el 0 → 100644
;;;; -*-coding: utf-8;-*-
;;;;
;;;; Copyright (C) 2020 David Byers
;;;;
;;;; This program is free software: you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License as
;;;; published by the Free Software Foundation, either version 3 of
;;;; the License, or (at your option) any later version.
;;;;
;;;; This program is distributed in the hope that it will be useful,
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;;;; General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU General Public License
;;;; along with this program. If not, see
;;;; <https://www.gnu.org/licenses/>.
;;; ========================================================================
;;; This program provides basic open location code support in emacs
;;; lisp. The support for recovering shortened codes depends on the
;;; request library and uses Open Streetmap; please check the terms of
;;; use for the service to ensure that you remain compliant.
;;;
;;; All methods required by the open location code specification are
;;; provided in some form. The implementation passed the tests present
;;; in the open location code github repository at the time of writing
;;; almost cleanly -- there are some minor rounding issues in decode.
;;;
;;; olc-encode encodes latitude and longitude to any length code.
;;; olc-decode decodes any length code (without reference location).
;;; olc-recover recovers shortened codes
;;;
;;; olc-is-valid checks for valid codes (long or short).
;;; olc-is-short checks for valid short codes.
;;; olc-is-full checks for valid full codes.
;;; olc-valid-digits checks for valid digits.
;;;
;;; There is no support for shortening codes.
;;; ========================================================================
;; This is me being dragged kicking and screaming into the 21st
;; century because the alternative is to include my own structured
;; data code -- which would be overkill -- or do it manually -- which is
;; a pain in the read end. So cl-lib it is.
(require 'cl-lib)
(require 'request nil t)
;; ========================================================================
;; Errors raised by this package
;; ========================================================================
(define-error 'olc-error "Error in open location code.")
(define-error 'olc-parse-error "Parse error in open location code" 'olc-error)
(define-error 'olc-decode-error "Error decoding open location code" 'olc-error)
(define-error 'olc-encode-error "Error encoding open location code" 'olc-error)
;; ========================================================================
;; Mapping of digits to base 20 values
;; ========================================================================
(defvar olc-value-mapping "23456789CFGHJMPQRVWX"
"Mapping from values to olc base 20 digits.")
(defvar olc-digit-mapping (let ((count 0))
(mapcan (lambda (letter)
(prog1 (list (cons letter count)
(cons (downcase letter) count))
(setq count (1+ count))))
olc-value-mapping))
"Mapping from olc base 20 digits to values.")
(defsubst olc-digit-value (digit)
"Return the base 20 value of a digit."
(cdr (assq digit olc-digit-mapping)))
(defsubst olc-value-digit (value)
"Return the digit for a value up to 19."
(elt olc-value-mapping value))
;; ========================================================================
;; Data structures
;; ========================================================================
(cl-defstruct (olc-parse (:copier nil)
(:constructor olc-parse-create))
(pairs nil :read-only t)
(grid nil :read-only t)
(short nil :read-only t)
(precision nil :read-only t)
(code nil :read-only t))
(defsubst olc-parse-length (parse)
"Get length from a parsed open location code PARSE."
(+ (* 2 (length (olc-parse-pairs parse)))
(length (olc-parse-grid parse))))
(cl-defstruct (olc-area (:copier nil)
(:constructor olc-area-create))
(latlo nil :read-only t)
(lonlo nil :read-only t)
(lathi nil :read-only t)
(lonhi nil :read-only t))
(defsubst olc-area-lat (area)
"Get center latitute of AREA."
(min (+ (/ (- (olc-area-lathi area) (olc-area-latlo area)) 2) (olc-area-latlo area)) 90))
(defsubst olc-area-lon (area)
"Get center longitude of AREA."
(min (+ (/ (- (olc-area-lonhi area) (olc-area-lonlo area)) 2) (olc-area-lonlo area)) 180))
;; ========================================================================
;; (Mostly) internal functions
;; ========================================================================
(defmacro olc-transform-error (spec &rest body)
"Catch some errors and throw others."
(declare (indent 1))
`(condition-case nil
,@body
(,(elt spec 0) (signal ',(elt spec 1) (list ,@(cddr spec))))))
(defun olc-normalize-latitude (lat length)
"Normalize latitude LAT."
(setq lat (max -90 (min 90 lat)))
(when (= lat 90.0)
(setq lat (- lat (/ (olc-latitude-precision length) 2.0))))
lat)
(defun olc-normalize-longitude (lon)
"Normalize longitude LON."
(while (< lon -180) (setq lon (+ lon 360)))
(while (>= lon 180) (setq lon (- lon 360)))
lon)
(defun olc-latitude-precision (len)
"Compute latitude precision in code of length LEN."
(if (<= len 10)
(expt 20 (- (ffloor (+ 2 (/ len 2)))))
(/ (expt 20 -3) (expt 5 (- len 10)))))
(defun olc-parse-code (code)
"Parse an open location code CODE."
(if (olc-parse-p code)
code
(let ((pos 0)
(pairs nil)
(short nil)
(precision nil)
(grid nil)
(padding 0))
;; Parse up to four initial pairs
(catch 'break
(while (< pos (length code))
(olc-transform-error (args-out-of-range olc-parse-error "code too short" code (1+ pos))
(cond ((eq (elt code pos) ?+) (throw 'break nil))
((eq (elt code pos) ?0) (throw 'break nil))
((= (length pairs) 4) (throw 'break nil))
((not (olc-valid-char (elt code pos)))
(signal 'olc-parse-error (list "invalid character" pos code)))
((not (olc-valid-char (elt code (1+ pos))))
(signal 'olc-parse-error (list "invalid character" (1+ pos) code)))
(t (setq pairs (cons (cons (elt code pos) (elt code (1+ pos))) pairs)))))
(setq pos (+ pos 2))))
;; Measure the padding
(when (string-match "0+" code pos)
(setq pos (match-end 0) padding (- (match-end 0) (match-beginning 0))))
;; Parse the separator
(olc-transform-error (args-out-of-range olc-parse-error "code too short" code pos)
(if (eq (elt code pos) ?+)
(setq pos (1+ pos))
(signal 'olc-parse-error (list "missing separator" pos code))))
;; Check the length of the padding
(unless (and (= (% padding 2) 0)
(<= (+ padding (* 2 (length pairs))) 8))
(signal 'olc-parse-error (list "incorrect padding length" pos code)))
;; Determine if the code is shortened or not
(setq short (< (+ (* 2 (length pairs)) padding) 8))
;; We cant be short and have padding (not sure why)
(when (and short (> padding 0))
(signal 'olc-parse-error (list "padded codes can't be shortened" pos code)))
;; Determine the precision of the code
(setq precision (- 8 padding))
;; Parse what's after the separator
(when (< pos (length code))
(when (> padding 0)
(signal 'olc-parse-error (list "padding followed by data" pos code)))
;; Parse one more pair
(olc-transform-error (args-out-of-range olc-parse-error "code too short" code (1+ pos))
(setq pairs (cons (cons (elt code pos) (elt code (1+ pos))) pairs)
pos (+ 2 pos)
precision (+ 2 precision)))
;; Parse grid
(while (< pos (length code))
(cond ((not (olc-valid-char (elt code pos)))
(signal 'olc-parse-error (list "invalid character" pos code)))
((>= (length grid) 5) (setq pos (1+ pos)))
(t (setq grid (cons (elt code pos) grid)
pos (1+ pos)
precision (1+ precision))))))
;; Check for an empty code
(unless pairs
(signal 'olc-parse-error (list "invalid code" 0 code)))
;; Return the result
(olc-parse-create :pairs (nreverse pairs)
:grid (nreverse grid)
:short short
:precision precision
:code code))))
;;; ========================================================================
;;; Public functions
;;; ========================================================================
(defsubst olc-valid-digits (value)
"Return non-nil if VALUE consists of valid digits.
VALUE can be a character or sequence of characters."
(condition-case nil
(if (characterp value)
(olc-digit-value char)
(mapc 'olc-digit-value char))
(error nil)))
(defun olc-is-valid (code)
"Return non-nil if CODE is a valid open location code."
(condition-case nil
(olc-parse-code code)
(olc-parse-error nil)))
(defun olc-is-short (code)
"Return non-nil if CODE is a valid short open location code.
Note that nil means the code is either not short, or it is
invalid."
(condition-case nil
(olc-parse-short (olc-parse-code code))
(olc-parse-error nil)))
(defun olc-is-full (code)
"Return non-nil if CODE is a valid long open location code.
Note that nil means the code is either not long, or it is
invalid."
(condition-case nil
(not (olc-parse-short (olc-parse-code code)))
(olc-parse-error nil)))
(defun olc-decode (code)
"Decode open location code CODE.
Returns a olc-parse structure or raises olc-parse-error if
the code is invalid or olc-decode-error if it cannot (legally) be
decoded.
Since this function uses floating point calculations, the results
are not identical to e.g. the C++ reference implementation. The
differences, however, are extremely small."
(let ((parse (olc-parse-code code))
(lat -90.0)
(lon -180.0)
(size 20.0))
;; We only deal with long codes
(when (olc-parse-short parse)
(signal 'olc-decode-error code))
;; Process the pairs
(mapc (lambda (pair)
(setq lat (+ lat (* size (olc-digit-value (car pair))))
lon (+ lon (* size (olc-digit-value (cdr pair))))
width size
height size
size (/ size 20.0)))
(olc-parse-pairs parse))
;; Process the grid
(when (olc-parse-grid parse)
(mapc (lambda (refine)
(setq width (/ width 4.0) height (/ height 5.0))
(let ((coord (olc-digit-value refine)))
(setq lat (+ lat (* height (/ coord 4)))
lon (+ lon (* width (% coord 4))))))
(olc-parse-grid parse)))
(olc-area-create :latlo lat :lonlo lon :lathi (+ lat height) :lonhi (+ lon width))))
(defun olc-encode (lat lon len)
"Encode LAT and LON as a LEN length open location code.
Returns an olc-area structure. Raises olc-encode-error if the
values cannot (legally) be encoded to the selected length."
(setq len (max 2 (min 15 len)))
(when (and (< len 11) (/= (% len 2) 0))
(signal 'olc-encode-error "invalid encoding length"))
(setq lat (olc-normalize-latitude lat length)
lon (olc-normalize-longitude lon))
(let ((code nil)
(invpreclat (* (expt 20 3) (expt 5 5)))
(invpreclon (* (expt 20 3) (expt 4 5))))
;; Convert lat and lon to integers for the computation
(setq lat (truncate (+ (* invpreclat lat) (* invpreclat 90)))
lon (truncate (+ (* invpreclon lon) (* invpreclon 180))))
;; Calculate the grid part if needed
(if (> len 10)
(dotimes (i 5)
(setq code (cons (olc-value-digit (+ (* (% lat 5) 4) (% lon 4)))
code)
lat (truncate lat 5)
lon (truncate lon 4)))
(setq lat (truncate lat (expt 5 5))
lon (truncate lon (expt 4 5))))
;; Calculate the pairs
(dotimes (i 5)
(when (eq i 1) (setq code (cons ?+ code)))
(setq code (cons (olc-value-digit (% lon 20)) code))
(setq code (cons (olc-value-digit (% lat 20)) code))
(setq lat (truncate lat 20)
lon (truncate lon 20)))
;; Truncate the code and add padding
(let ((truncate (< len 8)))
(setcdr (nthcdr (- len (if truncate 1 0)) code)
(nconc (make-list (max 0 (- 8 len)) ?0)
(when truncate (list ?+)))))
(apply 'string code)))
(defun olc-recover (code lat lon &optional format)
"Recover shortened code CODE from coordinates LAT and LON.
Optional FORMAT specifies the result format. 'latlon means return
the center latitude and longitude as a pair. 'area (the default)
means return an olc-area."
(let ((parse (olc-parse-code code)))
(if (olc-is-full parse)
(upcase code)
(setq lat (olc-normalize-latitude lat length)
lon (olc-normalize-longitude lon))
(let* ((padlen (- (olc-parse-precision parse)
(* 2 (length (olc-parse-pairs parse)))
(length (olc-parse-grid parse))))
(resolution (expt 20 (- 2 (/ padlen 2))))
(half-resolution (/ resolution 2.0))
(area (olc-decode (concat (substring (olc-encode lat lon 10) 0 padlen) code))))
(cond ((and (< (+ lat half-resolution) (olc-area-lat area))
(>= (- (olc-area-lat area) resolution) -90))
(setq lat (- (olc-area-lat area) resolution)))
((and (> (- lat half-resolution) (olc-area-lat area))
(<= (+ (olc-area-lat area) resolution) 90))
(setq lat (+ (olc-area-lat area) resolution)))
(t (setq lat (olc-area-lat area))))
(cond ((< (+ lon half-resolution) (olc-area-lon area))
(setq lon (- (olc-area-lon area) resolution)))
((> (- lon half-resolution) (olc-area-lon area))
(setq lon (+ (olc-area-lon area) resolution)))
(t (setq lon (olc-area-lon area))))
(cond ((eq format 'latlon) (cons lat lon))
(t (olc-encode lat lon (olc-parse-precision parse))))))))
(defun olc-recover-string (string &optional reference format)
"Recover a location from a shortened open location code and reference.
When called with one string argument, the string is assumed to
contain the code followed by whitespace, and then a reference
location as text.
When called with two string arguments, the first is assumed to be
the short code and the second is the reference location as text.
A symbol may be included as the last argument to select the
result format. See olc-recover for details."
(unless (fboundp 'request)
(error "request library is not loaded"))
(let (code resp)
(cond ((and (stringp string) (not (stringp reference)))
(setq format reference)
(if (string-match "^\\(\\S-+\\)\\s-+\\(.*\\)$" string)
(setq code (match-string 1 string)
reference (match-string 2 string))
(signal 'wrong-type-argument string)))
((and (stringp string) (stringp reference))
(setq code string))
(t (signal 'wrong-type-argument string)))
(setq resp (request "https://nominatim.openstreetmap.org/search"
:params `((q . ,reference)
(format . "json")
(limit . 1))
:parser 'json-read
:sync t))
(when (eq 200 (request-response-status-code resp))
(olc-recover code
(string-to-number (alist-get 'lat (elt (request-response-data resp) 0)))
(string-to-number (alist-get 'lon (elt (request-response-data resp) 0)))
format))))
# Test decoding Open Location Codes.
#
# Provides test cases for decoding valid codes.
#
# Format:
# code,length,latLo,lngLo,latHi,lngHi
7FG49Q00+,6,20.35,2.75,20.4,2.8
7FG49QCJ+2V,10,20.37,2.782125,20.370125,2.78225
7FG49QCJ+2VX,11,20.3701,2.78221875,20.370125,2.78225
7FG49QCJ+2VXGJ,13,20.370113,2.782234375,20.370114,2.78223632813
8FVC2222+22,10,47.0,8.0,47.000125,8.000125
4VCPPQGP+Q9,10,-41.273125,174.785875,-41.273,174.786
62G20000+,4,0.0,-180.0,1,-179
22220000+,4,-90,-180,-89,-179
7FG40000+,4,20.0,2.0,21.0,3.0
22222222+22,10,-90.0,-180.0,-89.999875,-179.999875
6VGX0000+,4,0,179,1,180
6FH32222+222,11,1,1,1.000025,1.00003125
# Special cases over 90 latitude and 180 longitude
CFX30000+,4,89,1,90,2
CFX30000+,4,89,1,90,2
62H20000+,4,1,-180,2,-179
62H30000+,4,1,-179,2,-178
CFX3X2X2+X2,10,89.9998750,1,90,1.0001250
# Test non-precise latitude/longitude value
6FH56C22+22,10,1.2000000000000028,3.4000000000000057,1.2001249999999999,3.4001250000000027
# Validate that digits after the first 15 are ignored when decoding
849VGJQF+VX7QR3J,15,37.5396691200,-122.3750698242,37.5396691600,-122.3750697021
849VGJQF+VX7QR3J7QR3J,15,37.5396691200,-122.3750698242,37.5396691600,-122.3750697021
# Test encoding Open Location Codes.
#
# Provides test cases for encoding latitude and longitude to codes.
#
# Format:
# latitude,longitude,length,expected code (empty if the input should cause an error)
20.375,2.775,6,7FG49Q00+
20.3700625,2.7821875,10,7FG49QCJ+2V
20.3701125,2.782234375,11,7FG49QCJ+2VX
20.3701135,2.78223535156,13,7FG49QCJ+2VXGJ
47.0000625,8.0000625,10,8FVC2222+22
-41.2730625,174.7859375,10,4VCPPQGP+Q9
0.5,-179.5,4,62G20000+
-89.5,-179.5,4,22220000+
20.5,2.5,4,7FG40000+
-89.9999375,-179.9999375,10,22222222+22
0.5,179.5,4,6VGX0000+
1,1,11,6FH32222+222
################################################################################
#
# Special cases over 90 latitude and 180 longitude
#
################################################################################
90,1,4,CFX30000+
92,1,4,CFX30000+
1,180,4,62H20000+
1,181,4,62H30000+
90,1,10,CFX3X2X2+X2
################################################################################
#
# Test non-precise latitude/longitude value
#
################################################################################
1.2,3.4,10,6FH56C22+22
################################################################################
#
# Validate that codes generated with a length exceeding 15 significant digits
# return a 15-digit code
#
################################################################################
37.539669125,-122.375069724,15,849VGJQF+VX7QR3J
37.539669125,-122.375069724,16,849VGJQF+VX7QR3J
37.539669125,-122.375069724,100,849VGJQF+VX7QR3J
################################################################################
#
# Test floating point representation/rounding errors.
#
################################################################################
35.6,3.033,10,8F75J22M+26
-48.71,142.78,8,4R347QRJ+
-70,163.7,8,3V252P22+
-2.804,7.003,13,6F9952W3+C6222
13.9,164.88,12,7V56WV2J+2222
-13.23,172.77,8,5VRJQQCC+
40.6,129.7,8,8QGFJP22+
-52.166,13.694,14,3FVMRMMV+JJ2222
-14,106.9,6,5PR82W00+
70.3,-87.64,13,C62J8926+22222
66.89,-106,10,95RPV2R2+22
2.5,-64.23,11,67JQGQ2C+222
-56.7,-47.2,14,38MJ8R22+222222
-34.45,-93.719,6,46Q8H700+
-35.849,-93.75,12,46P85722+C222
65.748,24.316,12,9GQ6P8X8+6C22
-57.32,130.43,12,3QJGMCJJ+2222
17.6,-44.4,6,789QJJ00+
-27.6,-104.8,6,554QC600+
41.87,-145.59,13,83HPVCC6+22222
-4.542,148.638,13,6R7CFJ5Q+66222
-37.014,-159.936,10,43J2X3P7+CJ
-57.25,125.49,15,3QJ7QF2R+2222222
48.89,-80.52,13,86WXVFRJ+22222
53.66,170.97,14,9V5GMX6C+222222
0.49,-76.97,15,67G5F2RJ+2222222
40.44,-36.7,12,89G5C8R2+2222
58.73,69.95,8,9JCFPXJ2+
16.179,150.075,12,7R8G53HG+J222
-55.574,-70.061,12,37PFCWGQ+CJ22
76.1,-82.5,15,C68V4G22+2222222
58.66,149.17,10,9RCFM56C+22
-67.2,48.6,6,3H4CRJ00+
-5.6,-54.5,14,6867CG22+222222
-34,145.5,14,4RR72G22+222222
-34.2,66.4,12,4JQ8RC22+2222
17.8,-108.5,6,759HRG00+
10.734,-168.294,10,722HPPM4+JC
-28.732,54.32,8,5H3P789C+
64.1,107.9,12,9PP94W22+2222
79.7525,6.9623,8,CFF8QX36+
-63.6449,-25.1475,8,398P9V43+
35.019,148.827,11,8R7C2R9G+JR2
71.132,-98.584,15,C6334CJ8+RC22222
53.38,-51.34,12,985C9MJ6+2222
-1.2,170.2,12,6VCGR622+2222
50.2,-162.8,11,922V6622+222
-25.798,-59.812,10,5862652Q+R6
81.654,-162.422,14,C2HVMH3H+J62222
-75.7,-35.4,8,29P68J22+
67.2,115.1,11,9PVQ6422+222
-78.137,-42.995,12,28HVV274+6222
-56.3,114.5,11,3PMPPG22+222
10.767,-62.787,13,772VQ687+R6222
-19.212,107.423,10,5PG9QCQF+66
21.192,-45.145,15,78HP5VR4+R222222
16.701,148.648,14,7R8CPJ2X+C62222
52.25,-77.45,15,97447H22+2222222
-68.54504,-62.81725,11,373VF53M+X4J
76.7,-86.172,12,C68MPR2H+2622
-6.2,96.6,13,6M5RRJ22+22222
59.32,-157.21,12,93F48QCR+2222
29.7,39.6,12,7GXXPJ22+2222
-18.32,96.397,10,5MHRM9JW+2R
-30.3,76.5,11,4JXRPG22+222
50.342,-112.534,15,95298FR8+RC22222
#
# There is no exact IEEE754 representation of 80.01 (or the negative), so test
# on either side.
#
80.0100000001,58.57,15,CHGW2H6C+2222222
80.0099999999,58.57,15,CHGW2H5C+X2RRRRR
-80.0099999999,58.57,15,2HFWXHRC+2222222
-80.0100000001,58.57,15,2HFWXHQC+X2RRRRR
#
# Add a few other examples.
#
47.000000080000000,8.00022229,15,8FVC2222+235235C
68.3500147997595,113.625636875353,15,9PWM9J2G+272FWJV
38.1176000887231,165.441989844555,15,8VC74C9R+2QX445C
-28.1217794010122,-154.066811473758,15,5337VWHM+77PR2GR
;;;; -*-coding: utf-8;-*-
;;;;
;;;; Copyright (C) 2020 David Byers
;;;;
;;;; This program is free software: you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License as
;;;; published by the Free Software Foundation, either version 3 of
;;;; the License, or (at your option) any later version.
;;;;
;;;; This program is distributed in the hope that it will be useful,
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;;;; General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU General Public License
;;;; along with this program. If not, see
;;;; <https://www.gnu.org/licenses/>.
(require 'cl-lib)
;; Decode still uses float arithmetic, so results can be slightly off
;; from the test cases. This is deemed acceptable.
(defvar olctest-decode-tolerance 0.0000000001)
(defun olctest-read-csv (filename)
"Read a CSV file with test data."
(let ((buffer (generate-new-buffer "*olctest*")))
(unwind-protect
(save-window-excursion
(set-buffer buffer)
(insert-file filename)
(goto-char (point-min))
(save-excursion (replace-string "full code" "fullcode"))
(unless (re-search-forward "^# Format.*:$" nil t)
(error "format line not found in test data"))
(forward-line 1)
(beginning-of-line)
(looking-at "^# *\\(\\S-*\\)")
(let ((columns (split-string (match-string 1) "," nil))
(cases nil))
(while (= 0 (forward-line 1))
(beginning-of-line)
(cond ((looking-at "^#"))
((looking-at "^\\s-*$"))
((looking-at "^\\(\\S-+\\)$")
(setq cases
(cons
(cl-mapcar (lambda (key val)
(cons (intern key)
(cond ((string-match "^[-0-9.]+$" val)
(string-to-number val))
((string= val "true") t)
((string= val "false") nil)
(t val))))
columns
(split-string (match-string 1) "," nil))
cases)))
(t (error (format "unable to parse test data: %s"
(buffer-substring
(point)
(progn (end-of-line) (point))))))))
cases))
(kill-buffer buffer))))
(defmacro olctest-run-tests (spec &rest body)
"Run open location code tests.
\(fn (VAR LIST) BODY...)"
(declare (indent 1) (debug ((form symbolp) body)))
(let ((data (gensym "$olctest")))
`(let ((,data (olctest-read-csv ,(elt spec 0)))
($olctest-results nil))
(setq foo ,data)
(dolist (,(elt spec 1) ,data)
,@body)
(olctest-report-results $olctest-results))))
(defun olctest-record-failure (case expected actual)
"Record a test failure."
(setq $olctest-results (cons `((case . ,case)
(expected . ,expected)
(actual . ,actual))
$olctest-results)))
(defun olctest-report-results (results)
"Report results from tests."
(if (null results)
t
(dolist (result results)
(princ (format "%s: expected %s got %s\n"
(mapconcat (lambda (val) (format "%s" (cdr val))) (alist-get 'case result) ",")
(alist-get 'expected result)
(alist-get 'actual result))))))
(defun olctest-encode ()
"Test encoding."
(olctest-run-tests ("encoding.csv" case)
(let ((code (olc-encode (alist-get 'latitude case)
(alist-get 'longitude case)
(alist-get 'length case))))
(unless (string= code (alist-get 'expected case))
(olctest-record-failure case (alist-get 'expected case) code)))))
(defun olctest-decode ()
"Test decoding."
(olctest-run-tests ("decoding.csv" case)
(let ((area (olc-decode (alist-get 'code case)))
(exp-latlo (alist-get 'latLo case))
(exp-lathi (alist-get 'latHi case))
(exp-lonlo (alist-get 'lngLo case))
(exp-lonhi (alist-get 'lngHi case))
(exp-len (alist-get 'length case)))
(unless (and (= exp-len (olc-code-length (alist-get 'code case)))
(< (abs (- (olc-area-latlo area) exp-latlo)) olctest-decode-tolerance)
(< (abs (- (olc-area-lathi area) exp-lathi)) olctest-decode-tolerance)
(< (abs (- (olc-area-lonlo area) exp-lonlo)) olctest-decode-tolerance)
(< (abs (- (olc-area-lonhi area) exp-lonhi)) olctest-decode-tolerance))
(olctest-record-failure case
(format "%d,%f,%f,%f,%f" exp-len exp-latlo exp-lonlo exp-lathi exp-lonhi)
(format "%d,%f,%f,%f,%f"
(olc-code-length (alist-get 'code case))
(olc-area-latlo area)
(olc-area-lonlo area)
(olc-area-lathi area)
(olc-area-lonhi area)))))))
(defun olctest-shortcodes ()
"Test recovering."
(olctest-run-tests ("shortCodeTests.csv" case)
(let ((fullcode (alist-get 'fullcode case))
(lat (alist-get 'lat case))
(lon (alist-get 'lng case))
(shortcode (alist-get 'shortcode case))
(test-type (alist-get 'test_type case)))
;; Test recover
(when (or (string= test-type "B") (string= test-type "R"))
(let ((recovered (olc-recover shortcode lat lon)))
(unless (string= recovered fullcode)
(olctest-record-failure case fullcode recovered))))
;; Test shorten
(when (or (string= test-type "B") (string= test-type "S"))
;; Shorten is not implemented
)
)))
(defun olctest-validity ()
"Test validity."
(olctest-run-tests ("validityTests.csv" case)
(let* ((code (alist-get 'code case))
(expected (list (alist-get 'isValid case)
(alist-get 'isShort case)
(alist-get 'isFull case)))
(actual (list (not (not (olc-is-valid code)))
(not (not (olc-is-short code)))
(not (not (olc-is-full code))))))
(unless (equal expected actual)
(olctest-record-failure case expected actual)))))
(defun olctest-run-all ()
"Run all tests."
(and (olctest-decode)
(olctest-encode)
(olctest-shortcodes)
(olctest-validity)))
# Test shortening and extending codes.
#
# Format:
# full code,lat,lng,shortcode,test_type
# test_type is R for recovery only, S for shorten only, or B for both.
9C3W9QCJ+2VX,51.3701125,-1.217765625,+2VX,B
# Adjust so we can't trim by 8 (+/- .000755)
9C3W9QCJ+2VX,51.3708675,-1.217765625,CJ+2VX,B
9C3W9QCJ+2VX,51.3693575,-1.217765625,CJ+2VX,B
9C3W9QCJ+2VX,51.3701125,-1.218520625,CJ+2VX,B
9C3W9QCJ+2VX,51.3701125,-1.217010625,CJ+2VX,B
# Adjust so we can't trim by 6 (+/- .0151)
9C3W9QCJ+2VX,51.3852125,-1.217765625,9QCJ+2VX,B
9C3W9QCJ+2VX,51.3550125,-1.217765625,9QCJ+2VX,B
9C3W9QCJ+2VX,51.3701125,-1.232865625,9QCJ+2VX,B
9C3W9QCJ+2VX,51.3701125,-1.202665625,9QCJ+2VX,B
# Added to detect error in recoverNearest functionality
8FJFW222+,42.899,9.012,22+,B
796RXG22+,14.95125,-23.5001,22+,B
# Reference location is in the 4 digit cell to the south.
8FVC2GGG+GG,46.976,8.526,2GGG+GG,B
# Reference location is in the 4 digit cell to the north.
8FRCXGGG+GG,47.026,8.526,XGGG+GG,B
# Reference location is in the 4 digit cell to the east.
8FR9GXGG+GG,46.526,8.026,GXGG+GG,B
# Reference location is in the 4 digit cell to the west.
8FRCG2GG+GG,46.526,7.976,G2GG+GG,B
# Added to detect errors recovering codes near the poles.
# This tests recovery function, but these codes won't shorten.
CFX22222+22,89.6,0.0,2222+22,R
2CXXXXXX+XX,-81.0,0.0,XXXXXX+XX,R
# Recovered full codes should be the full code
8FRCG2GG+GG,46.526,7.976,8FRCG2GG+GG,R
# Recovered full codes should be the uppercased full code
8FRCG2GG+GG,46.526,7.976,8frCG2GG+gG,R
# Test data for validity tests.
# Format of each line is:
# code,isValid,isShort,isFull
# Valid full codes:
8FWC2345+G6,true,false,true
8FWC2345+G6G,true,false,true
8fwc2345+,true,false,true
8FWCX400+,true,false,true
# Valid short codes:
WC2345+G6g,true,true,false
2345+G6,true,true,false
45+G6,true,true,false
+G6,true,true,false
# Invalid codes
G+,false,false,false
+,false,false,false
8FWC2345+G,false,false,false
8FWC2_45+G6,false,false,false
8FWC2η45+G6,false,false,false
8FWC2345+G6+,false,false,false
8FWC2345G6+,false,false,false
8FWC2300+G6,false,false,false
WC2300+G6g,false,false,false
WC2345+G,false,false,false
WC2300+,false,false,false
# Validate that codes at and exceeding 15 digits are still valid when all their
# digits are valid, and invalid when not.
849VGJQF+VX7QR3J,true,false,true
849VGJQF+VX7QR3U,false,false,false
849VGJQF+VX7QR3JW,true,false,true
849VGJQF+VX7QR3JU,false,false,false
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment