From e0bf27bc820f043cbd2e832ca1df00435a26dd59 Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Wed, 18 Feb 2026 14:28:07 +0000 Subject: [PATCH 1/4] Support using global Plus Codes to represent location MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Partially completes #16 This changes the handling of location strings by checking whether they are plus codes before processing them as lat/lon pairs. It also adds `open-location-code` as a dependency. ## Overview of Plus Codes Plus Codes are a way to represent geographical areas by using a string of characters and offer a system for addressing that is more human friendly than raw latitude and longitude coordinates. For example, the plus code **9C3XGVJC+2W** describes the location of [the British Library](https://plus.codes/9C3XGVJC+2W) in London. Compare this to the latitude and longitude values 51.530002, -0.127709, here written as Decimal Degrees, one of the many ways in which latitude and longitude are used. | Notation | Example (British Library) | Comment | | ----------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------- | | Decimal Degrees (DD) | 51.530002, -0.127709
*OR*
51.530002°, -0.127709°
*OR*
51.530002 N, 0.127709 W
| GIS | | Degrees Minutes Seconds (DMS) | 51°31′48.01″N, 00°07′39.75″W
*OR*
51°31′48″N, 00°07′40″W | Navigation and cartography | | Degrees Decimal Minutes (DDM) | 51° 31.8' N, 12° 46.254' W | Electronic navigation (aviation and maritime) | | ISO 6709 | +51.530002-000.127709/ | Can include altitude | | Well-Known Text (WKT) | POINT(-0.127709 51.530002) | Written with longitude first! | A plus code **does** encode location coordinates, but in a way that is unambiguous, culturally independent, and easy to decode. It also represents an **area** rather than a single point. This allows **flexible precision**: the first four digits describe a 1° lat by 1° lon area, and each subsequent digit reduces the area further and further. A 10 digit code describes an area approximately 14m x 14m, and adding 5 more digits describes an area of **4 x 14mm**. #### See also - [Open Location Code specification](https://github.com/google/open-location-code/blob/main/Documentation/Specification/olc_definition.adoc) - [plus.codes](https://plus.codes/) demo --- package-lock.json | 15 ++++++++++++++- package.json | 4 +++- src/map/utils.ts | 25 +++++++++++++++++++------ 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index b1a47e4..76d777d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,12 @@ "license": "MIT", "dependencies": { "@mapbox/mapbox-gl-rtl-text": "^0.3.0", - "maplibre-gl": "5.8.0" + "maplibre-gl": "5.8.0", + "open-location-code": "^1.0.3" }, "devDependencies": { "@types/node": "24.6.1", + "@types/open-location-code": "^1.0.1", "@typescript-eslint/eslint-plugin": "5.29.0", "@typescript-eslint/parser": "5.29.0", "builtin-modules": "3.3.0", @@ -793,6 +795,12 @@ "undici-types": "~7.13.0" } }, + "node_modules/@types/open-location-code": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/open-location-code/-/open-location-code-1.0.1.tgz", + "integrity": "sha512-txJKhrRpT2fw8RgZQUeT6qycBVIzRQ2zD7ecdLvc5R2QaeqSLcYIh1jxLSC+LjcpUT26Qyfw2UTttmhgk3sWmg==", + "dev": true + }, "node_modules/@types/supercluster": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", @@ -2196,6 +2204,11 @@ "wrappy": "1" } }, + "node_modules/open-location-code": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/open-location-code/-/open-location-code-1.0.3.tgz", + "integrity": "sha512-DBm14BSn40Ee241n80zIFXIT6+y8Tb0I+jTdosLJ8Sidvr2qONvymwqymVbHV2nS+1gkDZ5eTNpnOIVV0Kn2fw==" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", diff --git a/package.json b/package.json index 8ef8e15..70200f0 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "license": "MIT", "devDependencies": { "@types/node": "24.6.1", + "@types/open-location-code": "^1.0.1", "@typescript-eslint/eslint-plugin": "5.29.0", "@typescript-eslint/parser": "5.29.0", "builtin-modules": "3.3.0", @@ -29,6 +30,7 @@ }, "dependencies": { "@mapbox/mapbox-gl-rtl-text": "^0.3.0", - "maplibre-gl": "5.8.0" + "maplibre-gl": "5.8.0", + "open-location-code": "^1.0.3" } } diff --git a/src/map/utils.ts b/src/map/utils.ts index 9e70d0c..c8c3e0f 100644 --- a/src/map/utils.ts +++ b/src/map/utils.ts @@ -1,4 +1,5 @@ import { Value, NumberValue, StringValue, ListValue } from 'obsidian'; +import { OpenLocationCode } from 'open-location-code'; /** * Converts a Value to coordinate tuple [lat, lng] @@ -14,13 +15,25 @@ export function coordinateFromValue(value: Value | null): [number, number] | nul lng = parseCoordinate(value.get(1)); } } - // Handle string values (e.g., "34.1395597,-118.3870991" or "34.1395597, -118.3870991") + // Handle string values + // may be string coordinates (e.g., "34.1395597,-118.3870991" or "34.1395597, -118.3870991") + // or Plus Code (e.g. "9C3XGVHC+XW") else if (value instanceof StringValue) { - // Split by comma and handle various spacing - const parts = value.toString().trim().split(','); - if (parts.length >= 2) { - lat = parseCoordinate(parts[0].trim()); - lng = parseCoordinate(parts[1].trim()); + const str = value.toString().trim(); + + // Check for full Plus Code (e.g., "9C3XGVHC+XW") + if (OpenLocationCode.isFull(str)) { + const area = OpenLocationCode.decode(str); + lat = area.latitudeCenter; + lng = area.longitudeCenter; + } + // Fall back to comma-separated coordinates (e.g., "34.1395597, -118.3870991") + else { + const parts = str.split(','); + if (parts.length >= 2) { + lat = parseCoordinate(parts[0].trim()); + lng = parseCoordinate(parts[1].trim()); + } } } From 1079df52a20f8f6edd041e853a333607b69ae5fe Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Tue, 3 Mar 2026 16:48:50 +0000 Subject: [PATCH 2/4] Call OpenLocationCode functions as member functions, not static --- src/map/utils.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/map/utils.ts b/src/map/utils.ts index c8c3e0f..1d0e562 100644 --- a/src/map/utils.ts +++ b/src/map/utils.ts @@ -1,6 +1,8 @@ import { Value, NumberValue, StringValue, ListValue } from 'obsidian'; import { OpenLocationCode } from 'open-location-code'; +const olc = new OpenLocationCode(); + /** * Converts a Value to coordinate tuple [lat, lng] */ @@ -22,8 +24,8 @@ export function coordinateFromValue(value: Value | null): [number, number] | nul const str = value.toString().trim(); // Check for full Plus Code (e.g., "9C3XGVHC+XW") - if (OpenLocationCode.isFull(str)) { - const area = OpenLocationCode.decode(str); + if (olc.isFull(str)) { + const area = olc.decode(str); lat = area.latitudeCenter; lng = area.longitudeCenter; } From 5e491b49e198dcbf7ed04b1dffa2f68783d15acd Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Tue, 3 Mar 2026 16:58:11 +0000 Subject: [PATCH 3/4] Generate test places with pluscode locations --- maps | 1 + tests/generate_test_files.py | 53 ++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 120000 maps diff --git a/maps b/maps new file mode 120000 index 0000000..7f0f314 --- /dev/null +++ b/maps @@ -0,0 +1 @@ +maps/ \ No newline at end of file diff --git a/tests/generate_test_files.py b/tests/generate_test_files.py index c27dc9d..90b736b 100644 --- a/tests/generate_test_files.py +++ b/tests/generate_test_files.py @@ -145,6 +145,58 @@ def create_markdown_file(directory, filename, coordinates, place_type): f.write(content) +PLUS_CODE_PLACES = [ + {"name": "British Library", "plus_code": "9C3XGVJC+2W", "type": "[[Library]]"}, + {"name": "Eiffel Tower", "plus_code": "8FW4V75V+9R", "type": "[[Monument]]"}, + {"name": "Statue of Liberty", "plus_code": "87G7MXQ4+M6", "type": "[[Monument]]"}, + {"name": "Taj Mahal", "plus_code": "7JVW52GR+2R", "type": "[[Monument]]"}, + {"name": "Colosseum", "plus_code": "8FHJVFRR+3V", "type": "[[Monument]]"}, + {"name": "Great Wall of China", "plus_code": "8PGRCHJC+Q5", "type": "[[Monument]]"}, + {"name": "Great Pyramid of Giza", "plus_code": "7GXHX4HM+MM", "type": "[[Monument]]"}, + {"name": "Sydney Opera House", "plus_code": "4RRH46V8+74", "type": "[[Theater]]"}, + {"name": "Machu Picchu", "plus_code": "57R9RFP4+Q2", "type": "[[Monument]]"}, + {"name": "Burj Khalifa", "plus_code": "7HQQ57WF+VQ", "type": "[[Building]]"}, + {"name": "Christ the Redeemer", "plus_code": "589R2QXQ+6R", "type": "[[Monument]]"}, + {"name": "Big Ben", "plus_code": "9C3XGV2G+75", "type": "[[Monument]]"}, + {"name": "Golden Gate Bridge", "plus_code": "849VRG9C+XM", "type": "[[Monument]]"}, + {"name": "Acropolis of Athens", "plus_code": "8G95XPCG+J7", "type": "[[Monument]]"}, + {"name": "Petra", "plus_code": "8G2Q8CHV+CQ", "type": "[[Monument]]"}, + {"name": "Angkor Wat", "plus_code": "7P55CV78+2R", "type": "[[Temple]]"}, + {"name": "Sagrada Familia", "plus_code": "8FH4C53F+CQ", "type": "[[Cathedral]]"}, + {"name": "Mount Everest", "plus_code": "7MV8XWQG+62", "type": "[[Mountain]]"}, + {"name": "Hollywood Sign", "plus_code": "85634MMH+JC", "type": "[[Monument]]"}, + {"name": "Forbidden City", "plus_code": "8PFRW98W+GV", "type": "[[Palace]]"}, + {"name": "Stonehenge", "plus_code": "9C3W55HF+HG", "type": "[[Monument]]"}, +] + + +def create_plus_code_file(directory, place): + """Create a markdown file using a Plus Code for location.""" + content = f"""--- +category: "[[Places]]" +type: "{place['type']}" +location: "{place['plus_code']}" +--- +""" + filepath = directory / f"{place['name']}.md" + with open(filepath, 'w', encoding='utf-8') as f: + f.write(content) + + +def generate_plus_code_files(output_dir="generated_pluscode_places"): + """Generate test markdown files using Plus Codes for location.""" + script_dir = Path(__file__).parent + output_path = script_dir / output_dir + output_path.mkdir(exist_ok=True) + + print(f"Generating {len(PLUS_CODE_PLACES)} Plus Code test file(s) in {output_path}...") + + for place in PLUS_CODE_PLACES: + create_plus_code_file(output_path, place) + + print(f"✓ Successfully generated {len(PLUS_CODE_PLACES)} Plus Code file(s) in {output_path}/") + + def generate_test_files(count=100, output_dir="generated_places"): """Generate test markdown files with coordinates.""" # Create output directory @@ -203,6 +255,7 @@ def main(): sys.exit(1) generate_test_files(count) + generate_plus_code_files() if __name__ == "__main__": From 7e812c81771b745c6f67958d53accf225f186980 Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Tue, 3 Mar 2026 17:26:09 +0000 Subject: [PATCH 4/4] Consolidate test file generation into single command Replace separate generate_plus_code_files function with a unified generate_test_files that accepts a pluscode_count argument (default 20), outputting both coordinate and Plus Code files to the same directory. Co-Authored-By: Claude Opus 4.6 --- tests/generate_test_files.py | 69 ++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/tests/generate_test_files.py b/tests/generate_test_files.py index 90b736b..3774b1f 100644 --- a/tests/generate_test_files.py +++ b/tests/generate_test_files.py @@ -3,10 +3,11 @@ Generate test markdown files with valid coordinates for Obsidian Maps plugin testing. Usage: - python generate_test_files.py [count] + python generate_test_files.py [count] [pluscode_count] Arguments: - count: Number of files to generate (default: 100) + count: Number of coordinate files to generate (default: 100) + pluscode_count: Number of Plus Code files to generate (default: 20) """ import os @@ -183,32 +184,18 @@ def create_plus_code_file(directory, place): f.write(content) -def generate_plus_code_files(output_dir="generated_pluscode_places"): - """Generate test markdown files using Plus Codes for location.""" +def generate_test_files(count=100, pluscode_count=20, output_dir="generated_places"): + """Generate test markdown files with coordinates and Plus Code locations.""" + # Create output directory script_dir = Path(__file__).parent output_path = script_dir / output_dir output_path.mkdir(exist_ok=True) - print(f"Generating {len(PLUS_CODE_PLACES)} Plus Code test file(s) in {output_path}...") - - for place in PLUS_CODE_PLACES: - create_plus_code_file(output_path, place) - - print(f"✓ Successfully generated {len(PLUS_CODE_PLACES)} Plus Code file(s) in {output_path}/") - + print(f"Generating {count} coordinate files in {output_path}...") -def generate_test_files(count=100, output_dir="generated_places"): - """Generate test markdown files with coordinates.""" - # Create output directory - script_dir = Path(__file__).parent - output_path = script_dir / output_dir - output_path.mkdir(exist_ok=True) - - print(f"Generating {count} test files in {output_path}...") - # Keep track of generated names to avoid duplicates generated_names = set() - + for i in range(count): # Generate unique place name attempt = 0 @@ -221,29 +208,40 @@ def generate_test_files(count=100, output_dir="generated_places"): else: # If we can't find a unique name, append a number place_name = f"{generate_random_place_name()} {i}" - + # Generate coordinates and type coordinates = generate_coordinates() place_type = generate_place_type() - + # Create filename filename = f"{place_name}.md" - + # Create the file create_markdown_file(output_path, filename, coordinates, place_type) - + # Print progress for large batches if (i + 1) % 1000 == 0: print(f" Generated {i + 1} files...") - - print(f"✓ Successfully generated {count} files in {output_path}/") + + print(f"✓ Successfully generated {count} coordinate files in {output_path}/") + + # Generate Plus Code files + places = PLUS_CODE_PLACES[:pluscode_count] + print(f"Generating {len(places)} Plus Code file(s) in {output_path}...") + + for place in places: + create_plus_code_file(output_path, place) + + print(f"✓ Successfully generated {len(places)} Plus Code file(s) in {output_path}/") + return output_path def main(): """Main entry point.""" count = 100 # Default - + pluscode_count = 20 # Default + if len(sys.argv) > 1: try: count = int(sys.argv[1]) @@ -253,9 +251,18 @@ def main(): except ValueError: print(f"Error: Invalid count '{sys.argv[1]}'. Must be an integer.") sys.exit(1) - - generate_test_files(count) - generate_plus_code_files() + + if len(sys.argv) > 2: + try: + pluscode_count = int(sys.argv[2]) + if pluscode_count < 0: + print("Error: Plus code count must be a non-negative integer") + sys.exit(1) + except ValueError: + print(f"Error: Invalid pluscode_count '{sys.argv[2]}'. Must be an integer.") + sys.exit(1) + + generate_test_files(count, pluscode_count) if __name__ == "__main__":