// Copyright 2015 Tamás Gulácsi. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the 'License'); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an 'AS IS' BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package olc implements the Open Location Code algorithm to convert latitude and longitude coordinates // into a shorter sequence of letters and numbers. // // The aim is to provide something that can be used like an address in locations that lack them, because // the streets are unnamed. // // Codes represent areas, and the size of the area depends on the length of the code. The typical code // length is 10 digits, and represents an area of 1/8000 x 1/8000 degrees, or roughly 13.75 x 13.75 meters. // // See https://github.com/google/open-location-code. package olc import ( "errors" "fmt" "math" "strings" ) const ( // Separator is the character that separates the two parts of location code. Separator = '+' // Padding is the optional (left) padding character. Padding = '0' // Alphabet is the set of valid encoding characters. Alphabet = "23456789CFGHJMPQRVWX" encBase = len(Alphabet) maxCodeLen = 15 pairCodeLen = 10 gridCodeLen = maxCodeLen - pairCodeLen gridCols = 4 gridRows = 5 // First place value of the pairs (if the last pair value is 1). encBase^(pairs-1) pairFPV = 160000 // Precision of the pair part of the code, in 1/degrees. pairPrecision = 8000 // Full value of the latitude grid - gridRows**gridCodeLen. gridLatFullValue = 3125 // Full value of the longitude grid - gridCols**gridCodeLen. gridLngFullValue = 1024 // First place value of the latitude grid (if the last place is 1). gridRows^(gridCodeLen - 1) gridLatFPV = gridLatFullValue / gridRows // First place value of the longitude grid (if the last place is 1). gridCols^(gridCodeLen - 1) gridLngFPV = gridLngFullValue / gridCols // Latitude precision of a full length code. pairPrecision * gridRows**gridCodeLen finalLatPrecision = pairPrecision * gridLatFullValue // Longitude precision of a full length code. pairPrecision * gridCols**gridCodeLen finalLngPrecision = pairPrecision * gridLngFullValue latMax = 90 lngMax = 180 sepPos = 8 ) // CodeArea is the area represented by a location code. type CodeArea struct { LatLo, LngLo, LatHi, LngHi float64 Len int } // Center returns the (lat,lng) of the center of the area. func (area CodeArea) Center() (lat, lng float64) { return math.Min(area.LatLo+(area.LatHi-area.LatLo)/2, latMax), math.Min(area.LngLo+(area.LngHi-area.LngLo)/2, lngMax) } // Check checks whether the passed string is a valid OLC code. // It could be a full code (8FVC9G8F+6W), a padded code (8FVC0000+) or a code fragment (9G8F+6W). func Check(code string) error { if code == "" || len(code) == 1 && code[0] == Separator { return errors.New("empty code") } n := len(code) firstSep, firstPad := -1, -1 for i, r := range code { if firstPad != -1 { // Open Location Codes with less than eight digits can be suffixed with zeros with a "+" used as the final character. Zeros may not be followed by any other digit. switch r { case Padding: continue case Separator: if firstSep != -1 { return fmt.Errorf("extraneous separator @%d", i) } firstSep = i if n-1 == i { continue } } return fmt.Errorf("%c after zero @%d", r, i) } if '2' <= r && r <= '9' { continue } switch r { case 'C', 'F', 'G', 'H', 'J', 'M', 'P', 'Q', 'R', 'V', 'W', 'X', // Processing of Open Location Codes must be case insensitive. 'c', 'f', 'g', 'h', 'j', 'm', 'p', 'q', 'r', 'v', 'w', 'x': continue case Separator: // In addition to the above characters, a full Open Location Code can include a single "+" as a separator after the eighth digit. if firstSep != -1 { return fmt.Errorf("extra separator seen @%d", i) } if i > sepPos || i%2 == 1 { return fmt.Errorf("separator in illegal position @%d", i) } firstSep = i case Padding: if i == 0 { return errors.New("shouldn't start with padding character") } firstPad = i default: return fmt.Errorf("invalid char %c @%d", r, i) } } if firstSep == -1 { return errors.New("missing separator") } if n-firstSep-1 == 1 { return fmt.Errorf("only one char (%q) after separator", code[firstSep+1:]) } if firstPad != -1 { if firstSep < sepPos { return errors.New("short codes cannot have padding") } if len(code)-firstPad-1%2 == 1 { return errors.New("odd number of padding chars") } } return nil } // CheckShort checks whether the passed string is a valid short code. // If it is valid full code, then it returns ErrNotShort. func CheckShort(code string) error { if err := Check(code); err != nil { return err } if i := strings.IndexByte(code, Separator); i >= 0 && i < sepPos { return nil } return ErrNotShort } // CheckFull checks whether the passed string is a valid full code. // If it is short, it returns ErrShort. func CheckFull(code string) error { if err := CheckShort(code); err == nil { return ErrShort } else if err != ErrNotShort { return err } if firstLat := strings.IndexByte(Alphabet, upper(code[0])) * encBase; firstLat >= latMax*2 { return errors.New("latitude outside range") } if len(code) == 1 { return nil } if firstLong := strings.IndexByte(Alphabet, upper(code[1])) * encBase; firstLong >= lngMax*2 { return errors.New("longitude outside range") } return nil } func upper(b byte) byte { if 'c' <= b && b <= 'x' { return b + 'C' - 'c' } return b } // StripCode strips the padding and separator characters from the code. // // The code is truncated to the first 15 digits, as Decode won't use more, // to avoid underflow errors. func StripCode(code string) string { code = strings.Map( func(r rune) rune { if r == Separator || r == Padding { return -1 } return rune(upper(byte(r))) }, code) if len(code) > maxCodeLen { return code[:maxCodeLen] } return code } // Because the OLC codes are an area, they can't start at 180 degrees, because they would then have something > 180 as their upper bound. // Basically, what you have to do is normalize the longitude - so you need to change 180 degrees to -180 degrees. func normalize(value, max float64) float64 { for value < -max { value += 2 * max } for value >= max { value -= 2 * max } return value } // clipLatitude forces the latitude into the valid range. func clipLatitude(lat float64) float64 { if lat > latMax { return latMax } if lat < -latMax { return -latMax } return lat } func normalizeLat(value float64) float64 { return normalize(value, latMax) } func normalizeLng(value float64) float64 { return normalize(value, lngMax) }